tiny-http-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +778 -0
  3. package/dist/auth.d.ts +69 -0
  4. package/dist/auth.js +261 -0
  5. package/dist/cli.d.ts +20 -0
  6. package/dist/cli.js +465 -0
  7. package/dist/composition.json +25 -0
  8. package/dist/express-middleware.d.ts +18 -0
  9. package/dist/express-middleware.js +91 -0
  10. package/dist/http-server.d.ts +48 -0
  11. package/dist/http-server.js +263 -0
  12. package/dist/http-transport.d.ts +164 -0
  13. package/dist/http-transport.js +897 -0
  14. package/dist/index.d.ts +13 -0
  15. package/dist/index.js +6 -0
  16. package/dist/load-oauth-verifier.d.ts +6 -0
  17. package/dist/load-oauth-verifier.js +43 -0
  18. package/dist/parse-body.d.ts +22 -0
  19. package/dist/parse-body.js +150 -0
  20. package/dist/session.d.ts +18 -0
  21. package/dist/session.js +37 -0
  22. package/dist/sse.d.ts +11 -0
  23. package/dist/sse.js +22 -0
  24. package/dist/test-support.d.ts +10 -0
  25. package/dist/test-support.js +398 -0
  26. package/dist/testing.d.ts +59 -0
  27. package/dist/testing.js +191 -0
  28. package/node_modules/auth-store/LICENSE +21 -0
  29. package/node_modules/auth-store/README.md +62 -0
  30. package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
  31. package/node_modules/auth-store/dist/create-secret-store.js +44 -0
  32. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +47 -0
  33. package/node_modules/auth-store/dist/encrypted-file-store.js +303 -0
  34. package/node_modules/auth-store/dist/error-codes.d.ts +1 -0
  35. package/node_modules/auth-store/dist/error-codes.js +5 -0
  36. package/node_modules/auth-store/dist/index.d.ts +7 -0
  37. package/node_modules/auth-store/dist/index.js +4 -0
  38. package/node_modules/auth-store/dist/keychain-store.d.ts +25 -0
  39. package/node_modules/auth-store/dist/keychain-store.js +154 -0
  40. package/node_modules/auth-store/dist/provider-store.d.ts +14 -0
  41. package/node_modules/auth-store/dist/provider-store.js +78 -0
  42. package/node_modules/auth-store/dist/types.d.ts +22 -0
  43. package/node_modules/auth-store/dist/types.js +1 -0
  44. package/node_modules/auth-store/package.json +27 -0
  45. package/node_modules/mcp-oauth/LICENSE +21 -0
  46. package/node_modules/mcp-oauth/README.md +70 -0
  47. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
  48. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +169 -0
  49. package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
  50. package/node_modules/mcp-oauth/dist/client/authorization-state.js +47 -0
  51. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
  52. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +627 -0
  53. package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
  54. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +207 -0
  55. package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
  56. package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
  57. package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
  58. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +164 -0
  59. package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
  60. package/node_modules/mcp-oauth/dist/client/types.js +1 -0
  61. package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
  62. package/node_modules/mcp-oauth/dist/index.js +7 -0
  63. package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
  64. package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
  65. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +32 -0
  66. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +388 -0
  67. package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
  68. package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
  69. package/node_modules/mcp-oauth/package.json +33 -0
  70. package/node_modules/tiny-mcp-client/LICENSE +21 -0
  71. package/node_modules/tiny-mcp-client/README.md +104 -0
  72. package/node_modules/tiny-mcp-client/dist/index.d.ts +660 -0
  73. package/node_modules/tiny-mcp-client/dist/index.js +3870 -0
  74. package/node_modules/tiny-mcp-client/package.json +30 -0
  75. package/package.json +63 -0
package/dist/cli.js ADDED
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { parseArgs } from "node:util";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createHttpServer } from "./http-server.js";
6
+ import { loadOAuthVerifier } from "./load-oauth-verifier.js";
7
+ import packageJson from "../package.json" with { type: "json" };
8
+ function readPackageInfo() {
9
+ return {
10
+ name: packageJson.name,
11
+ version: packageJson.version
12
+ };
13
+ }
14
+ const packageInfo = readPackageInfo();
15
+ const HELP_TEXT = [
16
+ "Usage: tiny-http-mcp-server [options]",
17
+ "",
18
+ "Options:",
19
+ " --port <port> Port to listen on (default: 3000)",
20
+ " --hostname <hostname> Hostname to bind to (default: 127.0.0.1)",
21
+ " --path <path> HTTP path to serve MCP on (default: /mcp)",
22
+ " --stateless Disable session support",
23
+ " --json-response Return application/json for POST responses",
24
+ " --allowed-host <host> Allowed Host header value (repeatable; default: localhost loopback hosts)",
25
+ " --allowed-origin <url> Allowed CORS Origin value (repeatable)",
26
+ " --max-request-bytes <bytes>",
27
+ " Maximum JSON request body size",
28
+ " --max-batch-size <count>",
29
+ " Maximum JSON-RPC batch member count",
30
+ " --max-sessions <count> Maximum active sessions",
31
+ " --session-ttl-ms <ms> Expire sessions after this idle duration",
32
+ " --max-streams-per-session <count>",
33
+ " Maximum concurrent GET SSE streams per session",
34
+ " --max-stream-buffer-bytes <bytes>",
35
+ " Maximum buffered bytes per GET SSE stream (default: 1048576)",
36
+ " --max-sse-event-history <count>",
37
+ " Number of SSE events retained for Last-Event-ID replay",
38
+ " --sse-keep-alive-ms <ms>",
39
+ " GET SSE keepalive interval (default: 30000; 0 disables)",
40
+ " --max-concurrent-tool-calls <count>",
41
+ " Maximum concurrent tool calls across sessions",
42
+ " --trusted-proxy Trust X-Forwarded-Proto and X-Forwarded-Host",
43
+ " --request-timeout-ms <ms>",
44
+ " Node HTTP request timeout",
45
+ " --headers-timeout-ms <ms>",
46
+ " Node HTTP headers timeout",
47
+ " --keep-alive-timeout-ms <ms>",
48
+ " Node HTTP keep-alive timeout",
49
+ " --shutdown-grace-ms <ms>",
50
+ " Grace period before force-closing connections (default: 10000)",
51
+ " --oauth-resource <uri> Enable OAuth mode with this canonical resource URI",
52
+ " --oauth-authorization-server <issuer>",
53
+ " Authorization server issuer URL (repeatable)",
54
+ " --oauth-supported-scope <scope>",
55
+ " Scope published in OAuth metadata (repeatable)",
56
+ " --oauth-required-scope <scope>",
57
+ " Scope required on MCP requests (repeatable)",
58
+ " --oauth-bearer-method <method>",
59
+ " Bearer transport published in metadata (repeatable)",
60
+ " --oauth-verifier-module <path-or-file-url>",
61
+ " Module that exports the TokenVerifier implementation",
62
+ " --oauth-verifier-export <name>",
63
+ " Export name to load from the verifier module (default: default)",
64
+ " --version Show the package version",
65
+ " -h, --help Show this help message"
66
+ ].join("\n");
67
+ function parsePort(value) {
68
+ if (value === undefined) {
69
+ return 3000;
70
+ }
71
+ const port = parseDecimalInteger(value, "--port");
72
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
73
+ throw new Error("--port must be an integer between 0 and 65535.");
74
+ }
75
+ return port;
76
+ }
77
+ function parseAbsoluteUrl(value, flagName) {
78
+ try {
79
+ return new URL(value).toString();
80
+ }
81
+ catch {
82
+ throw new Error(`${flagName} must be an absolute URL.`);
83
+ }
84
+ }
85
+ function parseOrigin(value, flagName) {
86
+ try {
87
+ return new URL(value).origin;
88
+ }
89
+ catch {
90
+ throw new Error(`${flagName} must be an absolute URL.`);
91
+ }
92
+ }
93
+ function parseDecimalInteger(value, flagName) {
94
+ const trimmed = value.trim();
95
+ if (trimmed.length === 0) {
96
+ throw new Error(`${flagName} must be an integer.`);
97
+ }
98
+ for (const character of trimmed) {
99
+ const codePoint = character.codePointAt(0);
100
+ if (codePoint === undefined || codePoint < 48 || codePoint > 57) {
101
+ throw new Error(`${flagName} must be an integer.`);
102
+ }
103
+ }
104
+ return Number(trimmed);
105
+ }
106
+ function parseOptionalInteger(value, flagName, minimum) {
107
+ if (value === undefined) {
108
+ return undefined;
109
+ }
110
+ const parsed = parseDecimalInteger(value, flagName);
111
+ if (!Number.isInteger(parsed) || parsed < minimum) {
112
+ throw new Error(`${flagName} must be an integer greater than or equal to ${minimum}.`);
113
+ }
114
+ return parsed;
115
+ }
116
+ function hasConfiguredOAuthFlag(values) {
117
+ return [
118
+ values["oauth-resource"],
119
+ values["oauth-authorization-server"],
120
+ values["oauth-supported-scope"],
121
+ values["oauth-required-scope"],
122
+ values["oauth-bearer-method"],
123
+ values["oauth-verifier-module"],
124
+ values["oauth-verifier-export"]
125
+ ].some((value) => value !== undefined);
126
+ }
127
+ function parseRepeatableStrings(value, flagName) {
128
+ if (!Array.isArray(value) || value.length === 0) {
129
+ return undefined;
130
+ }
131
+ const normalized = [];
132
+ for (const item of value) {
133
+ if (typeof item !== "string") {
134
+ throw new Error(`${flagName} must be provided as a string.`);
135
+ }
136
+ const trimmed = item.trim();
137
+ if (trimmed.length === 0) {
138
+ throw new Error(`${flagName} must not be blank.`);
139
+ }
140
+ normalized.push(trimmed);
141
+ }
142
+ return normalized;
143
+ }
144
+ function parseCliOAuthOptions(values) {
145
+ const resource = values["oauth-resource"];
146
+ const authorizationServers = values["oauth-authorization-server"];
147
+ const verifierModule = values["oauth-verifier-module"];
148
+ const verifierExport = values["oauth-verifier-export"];
149
+ const hasOAuthFlags = hasConfiguredOAuthFlag(values);
150
+ if (typeof resource !== "string") {
151
+ if (hasOAuthFlags) {
152
+ throw new Error("--oauth-resource is required when configuring OAuth.");
153
+ }
154
+ return undefined;
155
+ }
156
+ if (!Array.isArray(authorizationServers) || authorizationServers.length === 0) {
157
+ throw new Error("--oauth-authorization-server must be provided at least once when --oauth-resource is set.");
158
+ }
159
+ if (typeof verifierModule !== "string" || verifierModule.length === 0) {
160
+ throw new Error("--oauth-verifier-module is required when --oauth-resource is set.");
161
+ }
162
+ const supportedScopes = parseRepeatableStrings(values["oauth-supported-scope"], "--oauth-supported-scope");
163
+ const requiredScopes = parseRepeatableStrings(values["oauth-required-scope"], "--oauth-required-scope");
164
+ const bearerMethods = parseRepeatableStrings(values["oauth-bearer-method"], "--oauth-bearer-method");
165
+ return {
166
+ resource: parseAbsoluteUrl(resource, "--oauth-resource"),
167
+ authorizationServers: authorizationServers.map((value) => parseAbsoluteUrl(value, "--oauth-authorization-server")),
168
+ ...(requiredScopes === undefined ? {} : { requiredScopes }),
169
+ ...(supportedScopes === undefined ? {} : { scopesSupported: supportedScopes }),
170
+ ...(bearerMethods === undefined ? {} : { bearerMethodsSupported: bearerMethods }),
171
+ verifierModule,
172
+ verifierExport: typeof verifierExport === "string" && verifierExport.length > 0 ? verifierExport : "default"
173
+ };
174
+ }
175
+ function parseCliOptions(args) {
176
+ const { values } = parseArgs({
177
+ args,
178
+ strict: true,
179
+ allowPositionals: false,
180
+ options: {
181
+ port: { type: "string" },
182
+ hostname: { type: "string" },
183
+ path: { type: "string" },
184
+ stateless: { type: "boolean" },
185
+ "json-response": { type: "boolean" },
186
+ "allowed-host": { type: "string", multiple: true },
187
+ "allowed-origin": { type: "string", multiple: true },
188
+ "max-request-bytes": { type: "string" },
189
+ "max-batch-size": { type: "string" },
190
+ "max-sessions": { type: "string" },
191
+ "session-ttl-ms": { type: "string" },
192
+ "max-streams-per-session": { type: "string" },
193
+ "max-stream-buffer-bytes": { type: "string" },
194
+ "max-sse-event-history": { type: "string" },
195
+ "sse-keep-alive-ms": { type: "string" },
196
+ "max-concurrent-tool-calls": { type: "string" },
197
+ "trusted-proxy": { type: "boolean" },
198
+ "request-timeout-ms": { type: "string" },
199
+ "headers-timeout-ms": { type: "string" },
200
+ "keep-alive-timeout-ms": { type: "string" },
201
+ "shutdown-grace-ms": { type: "string" },
202
+ "oauth-resource": { type: "string" },
203
+ "oauth-authorization-server": { type: "string", multiple: true },
204
+ "oauth-supported-scope": { type: "string", multiple: true },
205
+ "oauth-required-scope": { type: "string", multiple: true },
206
+ "oauth-bearer-method": { type: "string", multiple: true },
207
+ "oauth-verifier-module": { type: "string" },
208
+ "oauth-verifier-export": { type: "string" },
209
+ help: { type: "boolean", short: "h" },
210
+ version: { type: "boolean" }
211
+ }
212
+ });
213
+ const maxRequestBytes = parseOptionalInteger(values["max-request-bytes"], "--max-request-bytes", 1);
214
+ const maxBatchSize = parseOptionalInteger(values["max-batch-size"], "--max-batch-size", 1);
215
+ const maxSessions = parseOptionalInteger(values["max-sessions"], "--max-sessions", 1);
216
+ const sessionTtlMs = parseOptionalInteger(values["session-ttl-ms"], "--session-ttl-ms", 1);
217
+ const maxStreamsPerSession = parseOptionalInteger(values["max-streams-per-session"], "--max-streams-per-session", 1);
218
+ const maxStreamBufferBytes = parseOptionalInteger(values["max-stream-buffer-bytes"], "--max-stream-buffer-bytes", 0);
219
+ const maxSseEventHistory = parseOptionalInteger(values["max-sse-event-history"], "--max-sse-event-history", 0);
220
+ const sseKeepAliveMs = parseOptionalInteger(values["sse-keep-alive-ms"], "--sse-keep-alive-ms", 0);
221
+ const maxConcurrentToolCalls = parseOptionalInteger(values["max-concurrent-tool-calls"], "--max-concurrent-tool-calls", 1);
222
+ const requestTimeoutMs = parseOptionalInteger(values["request-timeout-ms"], "--request-timeout-ms", 0);
223
+ const headersTimeoutMs = parseOptionalInteger(values["headers-timeout-ms"], "--headers-timeout-ms", 0);
224
+ const keepAliveTimeoutMs = parseOptionalInteger(values["keep-alive-timeout-ms"], "--keep-alive-timeout-ms", 0);
225
+ const shutdownGraceMs = parseOptionalInteger(values["shutdown-grace-ms"], "--shutdown-grace-ms", 0) ?? 10_000;
226
+ return {
227
+ help: values.help ?? false,
228
+ version: values.version ?? false,
229
+ port: parsePort(values.port),
230
+ hostname: values.hostname ?? "127.0.0.1",
231
+ path: values.path ?? "/mcp",
232
+ stateless: values.stateless ?? false,
233
+ jsonResponse: values["json-response"] ?? false,
234
+ ...(Array.isArray(values["allowed-host"]) && values["allowed-host"].length > 0
235
+ ? { allowedHosts: [...values["allowed-host"]] }
236
+ : {}),
237
+ ...(Array.isArray(values["allowed-origin"]) && values["allowed-origin"].length > 0
238
+ ? {
239
+ allowedOrigins: values["allowed-origin"].map((value) => parseOrigin(value, "--allowed-origin"))
240
+ }
241
+ : {}),
242
+ ...(maxRequestBytes === undefined ? {} : { maxRequestBytes }),
243
+ ...(maxBatchSize === undefined ? {} : { maxBatchSize }),
244
+ ...(maxSessions === undefined ? {} : { maxSessions }),
245
+ ...(sessionTtlMs === undefined ? {} : { sessionTtlMs }),
246
+ ...(maxStreamsPerSession === undefined ? {} : { maxStreamsPerSession }),
247
+ ...(maxStreamBufferBytes === undefined ? {} : { maxStreamBufferBytes }),
248
+ ...(maxSseEventHistory === undefined ? {} : { maxSseEventHistory }),
249
+ ...(sseKeepAliveMs === undefined ? {} : { sseKeepAliveMs }),
250
+ ...(maxConcurrentToolCalls === undefined ? {} : { maxConcurrentToolCalls }),
251
+ trustedProxy: values["trusted-proxy"] ?? false,
252
+ ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
253
+ ...(headersTimeoutMs === undefined ? {} : { headersTimeoutMs }),
254
+ ...(keepAliveTimeoutMs === undefined ? {} : { keepAliveTimeoutMs }),
255
+ shutdownGraceMs,
256
+ oauth: parseCliOAuthOptions(values)
257
+ };
258
+ }
259
+ function listenForShutdownSignals(listener) {
260
+ process.on("SIGINT", listener);
261
+ process.on("SIGTERM", listener);
262
+ return () => {
263
+ process.off("SIGINT", listener);
264
+ process.off("SIGTERM", listener);
265
+ };
266
+ }
267
+ function scheduleShutdownGrace(listener, graceMs) {
268
+ const timer = setTimeout(listener, graceMs);
269
+ return () => clearTimeout(timer);
270
+ }
271
+ function waitForShutdown(shutdown, forceShutdown, graceMs, listenForSignals, scheduleGrace) {
272
+ return new Promise((resolve, reject) => {
273
+ let shutdownStarted = false;
274
+ let settled = false;
275
+ let cancelGrace = () => undefined;
276
+ let removeSignalListeners = () => undefined;
277
+ const finish = (forced) => {
278
+ if (settled) {
279
+ return;
280
+ }
281
+ settled = true;
282
+ cancelGrace();
283
+ removeSignalListeners();
284
+ resolve(forced);
285
+ };
286
+ const force = () => {
287
+ if (settled) {
288
+ return;
289
+ }
290
+ try {
291
+ forceShutdown();
292
+ finish(true);
293
+ }
294
+ catch {
295
+ finish(true);
296
+ }
297
+ };
298
+ const onSignal = () => {
299
+ if (shutdownStarted) {
300
+ force();
301
+ return;
302
+ }
303
+ shutdownStarted = true;
304
+ cancelGrace = scheduleGrace(force, graceMs);
305
+ void shutdown().then(() => finish(false), (error) => {
306
+ if (settled) {
307
+ return;
308
+ }
309
+ settled = true;
310
+ cancelGrace();
311
+ removeSignalListeners();
312
+ reject(error);
313
+ });
314
+ };
315
+ removeSignalListeners = listenForSignals(onSignal);
316
+ });
317
+ }
318
+ export function isCliInvocation(argv, moduleUrl, realpath = realpathSync) {
319
+ const entry = argv.at(1);
320
+ if (typeof entry !== "string") {
321
+ return false;
322
+ }
323
+ const candidates = [pathToFileURL(entry).href];
324
+ try {
325
+ candidates.push(pathToFileURL(realpath(entry)).href);
326
+ }
327
+ catch {
328
+ // Ignore resolution failures and keep the direct path candidate.
329
+ }
330
+ return candidates.includes(moduleUrl);
331
+ }
332
+ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
333
+ const createServer = dependencies.createServer ?? createHttpServer;
334
+ const loadVerifier = dependencies.loadOAuthVerifier ?? loadOAuthVerifier;
335
+ const stdout = dependencies.stdout ?? process.stdout;
336
+ const stderr = dependencies.stderr ?? process.stderr;
337
+ const customWaitForShutdown = dependencies.waitForShutdown;
338
+ const listenForSignals = dependencies.listenForShutdownSignals ?? listenForShutdownSignals;
339
+ const scheduleGrace = dependencies.scheduleShutdownGrace ?? scheduleShutdownGrace;
340
+ let handle;
341
+ let shutdownStarted = false;
342
+ let options;
343
+ try {
344
+ options = parseCliOptions(args);
345
+ }
346
+ catch (error) {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ stderr.write(`${message}\nRun with --help for usage.\n`);
349
+ return 1;
350
+ }
351
+ try {
352
+ if (options.help) {
353
+ stdout.write(`${HELP_TEXT}\n`);
354
+ return 0;
355
+ }
356
+ if (options.version) {
357
+ stdout.write(`${packageInfo.version}\n`);
358
+ return 0;
359
+ }
360
+ const oauth = options.oauth === undefined
361
+ ? undefined
362
+ : {
363
+ resource: options.oauth.resource,
364
+ authorizationServers: options.oauth.authorizationServers,
365
+ ...(options.oauth.requiredScopes !== undefined
366
+ ? { requiredScopes: options.oauth.requiredScopes }
367
+ : {}),
368
+ ...(options.oauth.scopesSupported !== undefined
369
+ ? { scopesSupported: options.oauth.scopesSupported }
370
+ : {}),
371
+ ...(options.oauth.bearerMethodsSupported !== undefined
372
+ ? { bearerMethodsSupported: options.oauth.bearerMethodsSupported }
373
+ : {}),
374
+ verifier: await loadVerifier({
375
+ modulePath: options.oauth.verifierModule,
376
+ exportName: options.oauth.verifierExport
377
+ })
378
+ };
379
+ const server = createServer({
380
+ name: packageInfo.name,
381
+ version: packageInfo.version,
382
+ ...(options.stateless ? { sessionIdGenerator: undefined } : {}),
383
+ ...(options.jsonResponse ? { enableJsonResponse: true } : {}),
384
+ ...(options.allowedHosts === undefined ? {} : { allowedHosts: options.allowedHosts }),
385
+ ...(options.allowedOrigins === undefined ? {} : { allowedOrigins: options.allowedOrigins }),
386
+ ...(options.maxRequestBytes === undefined
387
+ ? {}
388
+ : { maxRequestBytes: options.maxRequestBytes }),
389
+ ...(options.maxBatchSize === undefined ? {} : { maxBatchSize: options.maxBatchSize }),
390
+ ...(options.maxSessions === undefined ? {} : { maxSessions: options.maxSessions }),
391
+ ...(options.sessionTtlMs === undefined ? {} : { sessionTtlMs: options.sessionTtlMs }),
392
+ ...(options.maxStreamsPerSession === undefined
393
+ ? {}
394
+ : { maxStreamsPerSession: options.maxStreamsPerSession }),
395
+ ...(options.maxStreamBufferBytes === undefined
396
+ ? {}
397
+ : { maxStreamBufferBytes: options.maxStreamBufferBytes }),
398
+ ...(options.maxSseEventHistory === undefined
399
+ ? {}
400
+ : { maxSseEventHistory: options.maxSseEventHistory }),
401
+ ...(options.sseKeepAliveMs === undefined ? {} : { sseKeepAliveMs: options.sseKeepAliveMs }),
402
+ ...(options.maxConcurrentToolCalls === undefined
403
+ ? {}
404
+ : { maxConcurrentToolCalls: options.maxConcurrentToolCalls }),
405
+ ...(options.trustedProxy ? { trustedProxy: true } : {}),
406
+ ...(oauth === undefined ? {} : { oauth })
407
+ });
408
+ handle = await server.listenHttp({
409
+ port: options.port,
410
+ hostname: options.hostname,
411
+ path: options.path,
412
+ ...(options.requestTimeoutMs === undefined
413
+ ? {}
414
+ : { requestTimeoutMs: options.requestTimeoutMs }),
415
+ ...(options.headersTimeoutMs === undefined
416
+ ? {}
417
+ : { headersTimeoutMs: options.headersTimeoutMs }),
418
+ ...(options.keepAliveTimeoutMs === undefined
419
+ ? {}
420
+ : { keepAliveTimeoutMs: options.keepAliveTimeoutMs })
421
+ });
422
+ const shutdown = async () => {
423
+ shutdownStarted = true;
424
+ await handle?.close();
425
+ };
426
+ const forceShutdown = () => {
427
+ handle?.closeAllConnections();
428
+ };
429
+ const shutdownPromise = customWaitForShutdown === undefined
430
+ ? waitForShutdown(shutdown, forceShutdown, options.shutdownGraceMs, listenForSignals, scheduleGrace)
431
+ : undefined;
432
+ stdout.write(`${handle.url}\n`);
433
+ if (customWaitForShutdown === undefined) {
434
+ const forced = await shutdownPromise;
435
+ return forced ? 1 : 0;
436
+ }
437
+ else {
438
+ await customWaitForShutdown(shutdown);
439
+ }
440
+ return 0;
441
+ }
442
+ catch (error) {
443
+ if (handle !== undefined) {
444
+ try {
445
+ if (shutdownStarted) {
446
+ handle.closeAllConnections();
447
+ }
448
+ else {
449
+ await handle.close();
450
+ }
451
+ }
452
+ catch {
453
+ // Preserve the original CLI failure below.
454
+ }
455
+ }
456
+ const message = error instanceof Error ? error.message : String(error);
457
+ stderr.write(`${message}\n`);
458
+ return 1;
459
+ }
460
+ }
461
+ if (isCliInvocation(process.argv, import.meta.url)) {
462
+ runCli().then((code) => {
463
+ process.exit(code);
464
+ });
465
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "packages": [
4
+ {
5
+ "name": "auth-store",
6
+ "version": "0.0.1",
7
+ "license": "MIT"
8
+ },
9
+ {
10
+ "name": "mcp-oauth",
11
+ "version": "0.0.1",
12
+ "license": "MIT"
13
+ },
14
+ {
15
+ "name": "tiny-http-mcp-server",
16
+ "version": "0.1.0",
17
+ "license": "MIT"
18
+ },
19
+ {
20
+ "name": "tiny-mcp-client",
21
+ "version": "0.1.0",
22
+ "license": "MIT"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,18 @@
1
+ import { type RequestHandler } from "express";
2
+ import { type HttpServer, type ProtectedResourceMetadataOptions, type TinyHttpMcpServerOAuthOptions } from "./http-server.js";
3
+ import type { HttpObservabilityOptions } from "./http-transport.js";
4
+ export declare function createExpressMiddleware(server: HttpServer): RequestHandler;
5
+ export declare function createProtectedResourceMetadataRouter(options: ProtectedResourceMetadataOptions & {
6
+ path?: string;
7
+ }): RequestHandler;
8
+ export interface CreateExpressOAuthHandlersOptions {
9
+ path: string;
10
+ server: HttpServer;
11
+ oauth: TinyHttpMcpServerOAuthOptions;
12
+ trustedProxy?: boolean;
13
+ observability?: HttpObservabilityOptions;
14
+ }
15
+ export declare function createExpressOAuthHandlers(options: CreateExpressOAuthHandlersOptions): {
16
+ metadataMiddleware: RequestHandler;
17
+ mcpMiddleware: RequestHandler;
18
+ };
@@ -0,0 +1,91 @@
1
+ import express from "express";
2
+ import { authorizeBearerRequest } from "./auth.js";
3
+ import { createProtectedResourceMetadataDocument } from "./http-server.js";
4
+ import { PROTECTED_RESOURCE_METADATA_PATH } from "./auth.js";
5
+ import { PROTECTED_RESOURCE_METADATA_CACHE_CONTROL } from "./auth.js";
6
+ function normalizePath(path) {
7
+ if (path.length === 0 || path === "/") {
8
+ return "/";
9
+ }
10
+ if (path.includes("?") || path.includes("#")) {
11
+ throw new Error("path must not include a query or fragment");
12
+ }
13
+ if (!path.startsWith("/")) {
14
+ return `/${path}`;
15
+ }
16
+ return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
17
+ }
18
+ function setHardeningHeaders(res) {
19
+ res.set("X-Content-Type-Options", "nosniff");
20
+ res.set("Referrer-Policy", "no-referrer");
21
+ }
22
+ function readSessionId(headers) {
23
+ const value = headers["mcp-session-id"];
24
+ const sessionId = Array.isArray(value) ? value[0] : value;
25
+ return sessionId !== undefined && sessionId.length > 0 ? sessionId : undefined;
26
+ }
27
+ export function createExpressMiddleware(server) {
28
+ return async (req, res, next) => {
29
+ try {
30
+ await server.handleRequest(req, res);
31
+ }
32
+ catch (error) {
33
+ next(error);
34
+ }
35
+ };
36
+ }
37
+ export function createProtectedResourceMetadataRouter(options) {
38
+ const router = express.Router();
39
+ const document = createProtectedResourceMetadataDocument(options);
40
+ const metadataPaths = (() => {
41
+ const path = normalizePath(options.path ?? "/");
42
+ if (path === "/") {
43
+ return [PROTECTED_RESOURCE_METADATA_PATH];
44
+ }
45
+ return [`${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
46
+ })();
47
+ for (const metadataPath of metadataPaths) {
48
+ router.get(metadataPath, (_req, res) => {
49
+ setHardeningHeaders(res);
50
+ res.set("Cache-Control", PROTECTED_RESOURCE_METADATA_CACHE_CONTROL);
51
+ res.status(200).json(document);
52
+ });
53
+ }
54
+ return router;
55
+ }
56
+ export function createExpressOAuthHandlers(options) {
57
+ const mcpMiddleware = createExpressMiddleware(options.server);
58
+ const path = normalizePath(options.path);
59
+ return {
60
+ metadataMiddleware: createProtectedResourceMetadataRouter({
61
+ ...options.oauth,
62
+ path
63
+ }),
64
+ mcpMiddleware: async (req, res, next) => {
65
+ if (req.method === "OPTIONS") {
66
+ await mcpMiddleware(req, res, next);
67
+ return;
68
+ }
69
+ const authorization = await authorizeBearerRequest(req, {
70
+ ...options.oauth,
71
+ protectedResourcePath: path,
72
+ trustedProxy: options.trustedProxy
73
+ });
74
+ if (!authorization.ok) {
75
+ options.observability?.onEvent?.({
76
+ type: "auth.failure",
77
+ statusCode: authorization.statusCode,
78
+ ...(authorization.statusCode === 503 ? {} : { challenge: authorization.challenge }),
79
+ sessionId: readSessionId(req.headers)
80
+ });
81
+ if (authorization.statusCode !== 503) {
82
+ res.set("WWW-Authenticate", authorization.challenge);
83
+ }
84
+ setHardeningHeaders(res);
85
+ res.status(authorization.statusCode).end();
86
+ return;
87
+ }
88
+ await mcpMiddleware(req, res, next);
89
+ }
90
+ };
91
+ }
@@ -0,0 +1,48 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { type Server, type ServerOptions, type ToolDefinition, type CallToolResult, type ToolReturn, type TypedSchema } from "tiny-stdio-mcp-server";
3
+ import { type AuthenticatedIncomingMessage, type TokenVerifier, type VerifiedAccessToken, type RequestAuthInfo } from "./auth.js";
4
+ import { type HttpObservabilityEvent, type StreamableHttpTransportOptions } from "./http-transport.js";
5
+ export interface ProtectedResourceMetadataOptions {
6
+ resource: string | URL;
7
+ authorizationServers: readonly (string | URL)[];
8
+ bearerMethodsSupported?: readonly string[];
9
+ scopesSupported?: readonly string[];
10
+ }
11
+ export interface TinyHttpMcpServerOAuthOptions extends ProtectedResourceMetadataOptions {
12
+ requiredScopes?: readonly string[];
13
+ verifier: TokenVerifier;
14
+ }
15
+ export type HttpTransportOptions = ServerOptions & StreamableHttpTransportOptions & {
16
+ oauth?: TinyHttpMcpServerOAuthOptions;
17
+ };
18
+ export interface HttpListenOptions {
19
+ port?: number;
20
+ hostname?: string;
21
+ path?: string;
22
+ signal?: AbortSignal;
23
+ requestTimeoutMs?: number;
24
+ headersTimeoutMs?: number;
25
+ keepAliveTimeoutMs?: number;
26
+ }
27
+ export interface HttpServerHandle {
28
+ url: string;
29
+ port: number;
30
+ close(): Promise<void>;
31
+ closeAllConnections(): void;
32
+ }
33
+ export interface HttpServer extends Omit<Server, "tool" | "registerTool"> {
34
+ tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: HttpToolHandler<TIn, TOut>, outputSchema?: TypedSchema<TOut>): HttpServer;
35
+ registerTool<TIn, TOut = never>(definition: Omit<ToolDefinition<TIn, TOut>, "handler">, handler: HttpToolHandler<TIn, TOut>): HttpServer;
36
+ listenHttp(options?: HttpListenOptions): Promise<HttpServerHandle>;
37
+ handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
38
+ }
39
+ export interface HttpToolContext {
40
+ request: AuthenticatedIncomingMessage;
41
+ sessionId?: string;
42
+ auth?: RequestAuthInfo;
43
+ }
44
+ export type HttpToolHandler<T = Record<string, unknown>, TOut = ToolReturn> = (args: T, context: HttpToolContext) => Promise<TOut | CallToolResult> | TOut | CallToolResult;
45
+ export declare function createProtectedResourceMetadataDocument(options: ProtectedResourceMetadataOptions): Record<string, unknown>;
46
+ export declare function createHttpServer(options: HttpTransportOptions): HttpServer;
47
+ export type { RequestAuthInfo, TokenVerifier, VerifiedAccessToken };
48
+ export type { HttpObservabilityEvent };