Skip to content

Commit 1fae183

Browse files
authored
Merge pull request #5 from phughesmcr/dev
Fixes and improvements
2 parents 5e6367b + 424e195 commit 1fae183

File tree

16 files changed

+104
-90
lines changed

16 files changed

+104
-90
lines changed

.vscode/settings.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
{
22
"deno.enable": true,
3+
"deno.codeLens.implementations": true,
4+
"deno.codeLens.references": true,
5+
"deno.codeLens.referencesAllFunctions": true,
36
"editor.defaultFormatter": "denoland.vscode-deno",
47
"editor.formatOnSave": true,
58
"editor.codeActionsOnSave": {
6-
"source.fixAll": "always",
7-
"source.organizeImports": "always"
9+
"source.addMissingImports.ts": "explicit",
10+
"source.organizeImports": "explicit",
11+
"source.fixAll": "explicit",
812
},
913
"editor.rulers": [
1014
100
File renamed without changes.

deno.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@
2424
"type": "git",
2525
"url": "git+https://github.com/phughesmcr/deno-mcp-template.git"
2626
},
27+
"nodeModulesDir": "auto",
2728
"imports": {
2829
"$/": "./src/",
2930
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8",
3031
"@cliffy/flags": "jsr:@cliffy/flags@^1.0.0-rc.8",
31-
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.18.1",
32-
"@std/cli": "jsr:@std/cli@^1.0.22",
32+
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.22.0",
33+
"@std/cli": "jsr:@std/cli@^1.0.24",
3334
"@std/dotenv/load": "jsr:@std/dotenv@^0.225.5/load",
34-
"@std/path": "jsr:@std/path@^1.1.2",
35+
"@std/path": "jsr:@std/path@^1.1.3",
3536
"@std/ulid": "jsr:@std/ulid@^1.0.0",
3637
"fetch-to-node": "npm:fetch-to-node@^2.1.0",
37-
"hono": "jsr:@hono/hono@^4.9.8",
38+
"hono": "jsr:@hono/hono@^4.10.6",
3839
"hono-rate-limiter": "npm:hono-rate-limiter@^0.4.2",
39-
"zod": "npm:zod@^4.1.9",
40-
"zod-validation-error": "npm:zod-validation-error@^4.0.1"
40+
"zod": "npm:zod@^4.1.13",
41+
"zod-validation-error": "npm:zod-validation-error@^5.0.0"
4142
},
4243
"tasks": {
4344
"prep": "deno fmt && deno lint --fix && deno check main.ts && deno check src/**/*.ts",

main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env -S deno run -A
2+
/** @warning ^ You should narrow the allowed permissions in production. */
23

34
/**
45
* @description An example MCP server using Deno

src/app/app.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function createApp(mcp: McpServer, config: AppConfig): App {
3131
const start = async (): Promise<void> => {
3232
if (isRunning) return;
3333
if (startInProgress) return await startInProgress;
34+
if (stopInProgress) await stopInProgress;
3435

3536
startInProgress = (async () => {
3637
lastError = null;
@@ -52,6 +53,7 @@ export function createApp(mcp: McpServer, config: AppConfig): App {
5253
stdio.disconnect(),
5354
http.disconnect(),
5455
]);
56+
throw lastError;
5557
} finally {
5658
startInProgress = null;
5759
}
@@ -61,8 +63,9 @@ export function createApp(mcp: McpServer, config: AppConfig): App {
6163
};
6264

6365
const stop = async (): Promise<void> => {
66+
if (startInProgress) await startInProgress.catch(() => {});
6467
if (!isRunning) return;
65-
if (stopInProgress) return await stopInProgress;
68+
if (stopInProgress) return stopInProgress;
6669

6770
stopInProgress = (async () => {
6871
const results = await Promise.allSettled([
@@ -71,7 +74,10 @@ export function createApp(mcp: McpServer, config: AppConfig): App {
7174
]);
7275
isRunning = false;
7376
lastError = getRejected(results);
74-
if (lastError) throw lastError;
77+
if (lastError) {
78+
console.error(`${APP_NAME} stop encountered errors`);
79+
throw lastError;
80+
}
7581
})();
7682

7783
try {

src/app/cli.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,31 @@ import {
88
DEFAULT_HOSTNAME,
99
DEFAULT_PORT,
1010
} from "$/shared/constants.ts";
11-
import type { AppConfig } from "$/shared/types.ts";
11+
import type { AppConfig, Prettify } from "$/shared/types.ts";
1212
import { validateConfig } from "$/shared/validation.ts";
1313

1414
export type CliCommand = Awaited<ReturnType<typeof createCommand>>;
1515

16-
export type CliOptions =
16+
export type CliOptions = Prettify<
1717
& Omit<CliCommand["options"], "header" | "host" | "origin" | "noHttp" | "noStdio">
1818
& {
19+
http: boolean;
20+
stdio: boolean;
1921
headers: string[];
2022
allowedOrigins: string[];
2123
allowedHosts: string[];
22-
};
24+
}
25+
>;
2326

2427
/** Merges two arrays of strings, removing duplicates */
2528
function mergeArrays(a?: string[], b?: string[]): string[] {
2629
return [...new Set([...a ?? [], ...b ?? []])];
2730
}
2831

29-
async function createCommand() {
32+
/** Prefix for environment variables */
33+
const prefix = "MCP_";
34+
35+
function createCommand() {
3036
return new Command()
3137
.throwErrors()
3238
.name(APP_USAGE)
@@ -51,63 +57,56 @@ async function createCommand() {
5157
.option("--no-stdio", "Disable the STDIO server.", {
5258
conflicts: ["no-http"],
5359
})
54-
.env("NO_STDIO=<value:boolean>", "Disable the STDIO server.", {
55-
prefix: "MCP_",
56-
})
60+
.env("NO_STDIO=<value:boolean>", "Disable the STDIO server.", { prefix })
5761
// HTTP server
5862
.option("--no-http", "Disable the HTTP server.", {
5963
conflicts: ["no-stdio"],
6064
})
61-
.env("NO_HTTP=<value:boolean>", "Disable the HTTP server.", {
62-
prefix: "MCP_",
63-
})
65+
.env("NO_HTTP=<value:boolean>", "Disable the HTTP server.", { prefix })
6466
// Port
6567
.option("-p, --port <port:integer>", "Set the port.", {
6668
default: DEFAULT_PORT,
6769
conflicts: ["no-http"],
6870
})
69-
.env("PORT=<value:integer>", "Set the port.", { prefix: "MCP_" })
71+
.env("PORT=<value:integer>", "Set the port.", { prefix })
7072
// Hostname
7173
.option("-n, --hostname <hostname:string>", "Set the hostname.", {
7274
default: DEFAULT_HOSTNAME,
7375
conflicts: ["no-http"],
7476
})
75-
.env("HOSTNAME=<value:string>", "Set the hostname.", { prefix: "MCP_" })
77+
.action(function (options) {
78+
if (options.port < 1 || options.port > 65535) {
79+
throw new ValidationError("Port must be between 1 and 65535");
80+
}
81+
})
82+
.env("HOSTNAME=<value:string>", "Set the hostname.", { prefix })
7683
// Headers
7784
.option("-H, --header <header:string>", "Set a custom header.", {
7885
collect: true,
7986
conflicts: ["no-http"],
8087
})
81-
.env("HEADERS=<value:string[]>", "Set custom headers.", {
82-
prefix: "MCP_",
83-
})
88+
.env("HEADERS=<value:string[]>", "Set custom headers.", { prefix })
8489
// DNS rebinding
8590
.option("--dnsRebinding", "Enable DNS rebinding protection.", {
8691
default: false,
8792
conflicts: ["no-http"],
8893
depends: ["origin", "host"],
8994
})
90-
.env("DNS_REBINDING=<value:boolean>", "Enable DNS rebinding protection.", {
91-
prefix: "MCP_",
92-
})
95+
.env("DNS_REBINDING=<value:boolean>", "Enable DNS rebinding protection.", { prefix })
9396
// Allowed origins
9497
.option("--origin <origin:string>", "Allow an origin for DNS rebinding.", {
9598
collect: true,
9699
conflicts: ["no-http"],
97100
depends: ["dnsRebinding"],
98101
})
99-
.env("ALLOWED_ORIGINS=<value:string[]>", "Allowed origins for DNS rebinding.", {
100-
prefix: "MCP_",
101-
})
102+
.env("ALLOWED_ORIGINS=<value:string[]>", "Allowed origins for DNS rebinding.", { prefix })
102103
// Allowed hosts
103104
.option("--host <host:string>", "Allow a host for DNS rebinding.", {
104105
collect: true,
105106
conflicts: ["no-http"],
106107
depends: ["dnsRebinding"],
107108
})
108-
.env("ALLOWED_HOSTS=<value:string[]>", "Allowed hosts for DNS rebinding.", {
109-
prefix: "MCP_",
110-
})
109+
.env("ALLOWED_HOSTS=<value:string[]>", "Allowed hosts for DNS rebinding.", { prefix })
111110
.parse(Deno.args);
112111
}
113112

src/app/http/handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ function handleMCPError(c: Context, error?: unknown): Response {
2424
rpcError = RPCError.fromError(
2525
error,
2626
RPC_ERROR_CODES.INTERNAL_ERROR,
27-
sessionId ?? INVALID_SESSION_ID,
27+
sessionId,
2828
);
2929
} else {
30-
rpcError = RPCError.internalError(sessionId ?? INVALID_SESSION_ID);
30+
rpcError = RPCError.internalError(sessionId);
3131
}
3232

3333
// Map RPC error codes to HTTP status and return

src/app/http/hono.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function configureMiddleware(app: Hono, config: AppConfig["http"]): Hono {
6464
c.req.header(HEADER_KEYS.SESSION_ID) ||
6565
c.req.header("x-forwarded-for") ||
6666
c.req.header("x-real-ip") ||
67+
c.req.header("mcp-session-id") ||
6768
"unknown",
6869
}),
6970
);
@@ -105,15 +106,15 @@ function createRoutes(app: Hono, mcp: McpServer, transports: HTTPTransportManage
105106

106107
// Static Routes
107108
app.use("/static/*", serveStatic({ root: "./static" }));
108-
app.use("/.well-known/*", serveStatic({ root: "./static/.well-known" }));
109+
app.use("/.well-known/*", serveStatic({ root: "./static" }));
109110
app.use("/favicon.ico", serveStatic({ path: "./static/favicon.ico" }));
110111
app.get("/llms.txt", (c) => c.redirect("/.well-known/llms.txt"));
111112
app.get("/openapi.yaml", (c) => c.redirect("/.well-known/openapi.yaml"));
112113
// ... add more static routes here
113114
app.get("/", (c) => c.text(`${APP_NAME} running. See \`/llms.txt\` for machine-readable docs.`));
114115

115116
// 404 Route
116-
app.get("*", serveStatic({ path: "./static/404.html" }));
117+
app.notFound((c) => c.html(Deno.readTextFileSync("./static/404.html"), 404));
117118
}
118119

119120
/**
@@ -126,13 +127,14 @@ export function createHonoApp({ mcp, config, transports }: HonoAppSpec): Hono {
126127
configureMiddleware(app, config);
127128
createRoutes(app, mcp, transports);
128129
app.onError((err, c) => {
129-
return c.json(
130-
{
130+
const path = c.req.path;
131+
if (path === "/mcp") {
132+
return c.json({
131133
content: [{ type: "text", text: err.message }],
132134
isError: true,
133-
},
134-
HTTP_STATUS.INTERNAL_SERVER_ERROR,
135-
);
135+
}, HTTP_STATUS.INTERNAL_SERVER_ERROR);
136+
}
137+
return c.text(err.message, HTTP_STATUS.INTERNAL_SERVER_ERROR);
136138
});
137139
return app;
138140
}

src/app/http/mod.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22

33
import { APP_NAME } from "$/shared/constants.ts";
4-
import type { AppConfig, Transport } from "$/shared/types.ts";
4+
import type { HttpServerConfig, Transport } from "$/shared/types.ts";
55
import { createHonoApp } from "./hono.ts";
66
import { createHTTPTransportManager } from "./transport.ts";
77

@@ -11,13 +11,13 @@ import { createHTTPTransportManager } from "./transport.ts";
1111
* @param config - The HTTP server configuration
1212
* @returns The HTTP transport instance
1313
*/
14-
export function createHttpServer(mcp: McpServer, config: AppConfig["http"]): Transport {
14+
export function createHttpServer(mcp: McpServer, config: HttpServerConfig): Transport {
1515
const { enabled, hostname, port } = config;
1616
const transports = createHTTPTransportManager(config);
1717
const hono = createHonoApp({ mcp, config, transports });
1818
let server: Deno.HttpServer | null = null;
1919

20-
const connect = async () => {
20+
const connect = () => {
2121
if (!enabled || server !== null) return;
2222
server = Deno.serve({
2323
hostname,
@@ -35,9 +35,10 @@ export function createHttpServer(mcp: McpServer, config: AppConfig["http"]): Tra
3535
await transports.close();
3636
} catch {
3737
/* ignore */
38+
} finally {
39+
await server?.shutdown();
40+
server = null;
3841
}
39-
await server.shutdown();
40-
server = null;
4142
};
4243

4344
const isRunning = () => !!server;

src/app/http/transport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export function createHTTPTransportManager(config: AppConfig["http"]): HTTPTrans
110110

111111
const get = (sessionId: string) => transports.get(sessionId);
112112
const close = async () => {
113+
await releaseAll();
113114
if (eventStorePromise) {
114115
try {
115116
const store = await eventStorePromise;

0 commit comments

Comments
 (0)