@usestratus/mcp-aws 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 (110) hide show
  1. package/README.md +610 -0
  2. package/dist/auth/api-key.d.ts +29 -0
  3. package/dist/auth/api-key.d.ts.map +1 -0
  4. package/dist/auth/api-key.js +42 -0
  5. package/dist/auth/api-key.js.map +1 -0
  6. package/dist/auth/cognito.d.ts +26 -0
  7. package/dist/auth/cognito.d.ts.map +1 -0
  8. package/dist/auth/cognito.js +58 -0
  9. package/dist/auth/cognito.js.map +1 -0
  10. package/dist/auth/index.d.ts +11 -0
  11. package/dist/auth/index.d.ts.map +1 -0
  12. package/dist/auth/index.js +21 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/metadata.d.ts +30 -0
  15. package/dist/auth/metadata.d.ts.map +1 -0
  16. package/dist/auth/metadata.js +25 -0
  17. package/dist/auth/metadata.js.map +1 -0
  18. package/dist/auth/types.d.ts +8 -0
  19. package/dist/auth/types.d.ts.map +1 -0
  20. package/dist/auth/types.js +2 -0
  21. package/dist/auth/types.js.map +1 -0
  22. package/dist/codemode/executor.d.ts +29 -0
  23. package/dist/codemode/executor.d.ts.map +1 -0
  24. package/dist/codemode/executor.js +154 -0
  25. package/dist/codemode/executor.js.map +1 -0
  26. package/dist/codemode/index.d.ts +4 -0
  27. package/dist/codemode/index.d.ts.map +1 -0
  28. package/dist/codemode/index.js +3 -0
  29. package/dist/codemode/index.js.map +1 -0
  30. package/dist/codemode/types.d.ts +10 -0
  31. package/dist/codemode/types.d.ts.map +1 -0
  32. package/dist/codemode/types.js +195 -0
  33. package/dist/codemode/types.js.map +1 -0
  34. package/dist/compose.d.ts +57 -0
  35. package/dist/compose.d.ts.map +1 -0
  36. package/dist/compose.js +138 -0
  37. package/dist/compose.js.map +1 -0
  38. package/dist/context.d.ts +22 -0
  39. package/dist/context.d.ts.map +1 -0
  40. package/dist/context.js +39 -0
  41. package/dist/context.js.map +1 -0
  42. package/dist/deploy.d.ts +57 -0
  43. package/dist/deploy.d.ts.map +1 -0
  44. package/dist/deploy.js +281 -0
  45. package/dist/deploy.js.map +1 -0
  46. package/dist/disclosure/index.d.ts +3 -0
  47. package/dist/disclosure/index.d.ts.map +1 -0
  48. package/dist/disclosure/index.js +3 -0
  49. package/dist/disclosure/index.js.map +1 -0
  50. package/dist/disclosure/search.d.ts +15 -0
  51. package/dist/disclosure/search.d.ts.map +1 -0
  52. package/dist/disclosure/search.js +73 -0
  53. package/dist/disclosure/search.js.map +1 -0
  54. package/dist/disclosure/tier.d.ts +16 -0
  55. package/dist/disclosure/tier.d.ts.map +1 -0
  56. package/dist/disclosure/tier.js +69 -0
  57. package/dist/disclosure/tier.js.map +1 -0
  58. package/dist/errors.d.ts +34 -0
  59. package/dist/errors.d.ts.map +1 -0
  60. package/dist/errors.js +56 -0
  61. package/dist/errors.js.map +1 -0
  62. package/dist/events.d.ts +93 -0
  63. package/dist/events.d.ts.map +1 -0
  64. package/dist/events.js +28 -0
  65. package/dist/events.js.map +1 -0
  66. package/dist/gating/combinators.d.ts +10 -0
  67. package/dist/gating/combinators.d.ts.map +1 -0
  68. package/dist/gating/combinators.js +34 -0
  69. package/dist/gating/combinators.js.map +1 -0
  70. package/dist/gating/gates.d.ts +21 -0
  71. package/dist/gating/gates.d.ts.map +1 -0
  72. package/dist/gating/gates.js +74 -0
  73. package/dist/gating/gates.js.map +1 -0
  74. package/dist/gating/index.d.ts +3 -0
  75. package/dist/gating/index.d.ts.map +1 -0
  76. package/dist/gating/index.js +3 -0
  77. package/dist/gating/index.js.map +1 -0
  78. package/dist/index.d.ts +21 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +28 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/server.d.ts +161 -0
  83. package/dist/server.d.ts.map +1 -0
  84. package/dist/server.js +768 -0
  85. package/dist/server.js.map +1 -0
  86. package/dist/session/dynamo.d.ts +23 -0
  87. package/dist/session/dynamo.d.ts.map +1 -0
  88. package/dist/session/dynamo.js +92 -0
  89. package/dist/session/dynamo.js.map +1 -0
  90. package/dist/session/index.d.ts +4 -0
  91. package/dist/session/index.d.ts.map +1 -0
  92. package/dist/session/index.js +4 -0
  93. package/dist/session/index.js.map +1 -0
  94. package/dist/session/memory.d.ts +16 -0
  95. package/dist/session/memory.d.ts.map +1 -0
  96. package/dist/session/memory.js +43 -0
  97. package/dist/session/memory.js.map +1 -0
  98. package/dist/session/sqlite.d.ts +17 -0
  99. package/dist/session/sqlite.d.ts.map +1 -0
  100. package/dist/session/sqlite.js +79 -0
  101. package/dist/session/sqlite.js.map +1 -0
  102. package/dist/ssrf.d.ts +25 -0
  103. package/dist/ssrf.d.ts.map +1 -0
  104. package/dist/ssrf.js +88 -0
  105. package/dist/ssrf.js.map +1 -0
  106. package/dist/types.d.ts +110 -0
  107. package/dist/types.d.ts.map +1 -0
  108. package/dist/types.js +21 -0
  109. package/dist/types.js.map +1 -0
  110. package/package.json +50 -0
package/dist/server.js ADDED
@@ -0,0 +1,768 @@
1
+ import { McpServer as SdkMcpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { chainAuth } from "./auth/index.js";
4
+ import { buildResourceMetadata, buildWwwAuthenticateHeader } from "./auth/metadata.js";
5
+ import { FunctionExecutor, WorkerExecutor, generateTypes, normalizeCode, sanitizeToolName, } from "./codemode/index.js";
6
+ import { withContext } from "./context.js";
7
+ import { SearchIndex, getVisibleTools, handleGateUnlock, promoteToVisible, } from "./disclosure/index.js";
8
+ import { ToolTimeoutError } from "./errors.js";
9
+ import { McpEventEmitter } from "./events.js";
10
+ import { MemorySessionStore } from "./session/memory.js";
11
+ import { normalizeToolResult, } from "./types.js";
12
+ // ── Helpers ─────────────────────────────────────────────────────────
13
+ function createSession(id, auth) {
14
+ const now = Date.now();
15
+ return {
16
+ id,
17
+ visibleTools: new Set(),
18
+ unlockedGates: new Set(),
19
+ toolCallHistory: [],
20
+ auth,
21
+ metadata: {},
22
+ createdAt: now,
23
+ lastAccessedAt: now,
24
+ };
25
+ }
26
+ const UNAUTHED = { authenticated: false, roles: [], claims: {} };
27
+ function gateDenialResult(reason, hint) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: JSON.stringify({
33
+ error: "Permission denied",
34
+ reason,
35
+ ...(hint ? { hint } : {}),
36
+ }),
37
+ },
38
+ ],
39
+ isError: true,
40
+ };
41
+ }
42
+ function errorResult(message) {
43
+ return { content: [{ type: "text", text: message }], isError: true };
44
+ }
45
+ function parseNameVersion(input) {
46
+ const atIdx = input.lastIndexOf("@");
47
+ if (atIdx > 0) {
48
+ return { name: input.slice(0, atIdx), version: input.slice(atIdx + 1) };
49
+ }
50
+ return { name: input, version: "0.0.0" };
51
+ }
52
+ // ── McpServer ───────────────────────────────────────────────────────
53
+ export class McpServer {
54
+ #config;
55
+ #tools = new Map();
56
+ #searchIndex = new SearchIndex();
57
+ #codeMode;
58
+ #events = new McpEventEmitter();
59
+ #authProvider;
60
+ #sdkServer;
61
+ /** Active session for the current request (set by transports). */
62
+ #activeSession;
63
+ #activeAuth = UNAUTHED;
64
+ /**
65
+ * Create an MCP server.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const server = new McpServer("my-server@1.0.0");
70
+ * const server = new McpServer({ name: "my-server", version: "1.0.0" });
71
+ * ```
72
+ */
73
+ constructor(config) {
74
+ if (typeof config === "string") {
75
+ this.#config = parseNameVersion(config);
76
+ }
77
+ else {
78
+ this.#config = config;
79
+ }
80
+ this.#codeMode = this.#config.codeMode ?? { enabled: false };
81
+ }
82
+ // ── Auth ────────────────────────────────────────────────────────
83
+ /**
84
+ * Set the auth provider. Multiple calls or args chain automatically.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * server.auth(apiKey({ "sk-123": { roles: ["admin"] } }));
89
+ * server.auth(cognito({ userPoolId: "...", region: "us-east-1" }));
90
+ * ```
91
+ */
92
+ auth(...providers) {
93
+ if (providers.length === 0)
94
+ return this;
95
+ const newProvider = chainAuth(...providers);
96
+ if (this.#authProvider) {
97
+ this.#authProvider = chainAuth(this.#authProvider, newProvider);
98
+ }
99
+ else {
100
+ this.#authProvider = newProvider;
101
+ }
102
+ return this;
103
+ }
104
+ // ── Events ──────────────────────────────────────────────────────
105
+ /**
106
+ * Subscribe to server lifecycle events.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * server.on("tool:call", (e) => console.log(`${e.toolName} called`));
111
+ * server.on("auth:failure", () => metrics.increment("auth.failures"));
112
+ * ```
113
+ */
114
+ on(event, listener) {
115
+ this.#events.on(event, listener);
116
+ return this;
117
+ }
118
+ off(event, listener) {
119
+ this.#events.off(event, listener);
120
+ return this;
121
+ }
122
+ tool(name, paramsOrOptionsOrHandler, maybeHandler) {
123
+ let description = name;
124
+ let inputSchema;
125
+ let tier = "always";
126
+ let tags;
127
+ let gate;
128
+ let timeout;
129
+ let handler;
130
+ if (typeof paramsOrOptionsOrHandler === "function") {
131
+ // Overload 1: tool(name, handler)
132
+ handler = paramsOrOptionsOrHandler;
133
+ }
134
+ else if (paramsOrOptionsOrHandler instanceof z.ZodType) {
135
+ // Overload 2: tool(name, zodSchema, handler)
136
+ inputSchema = paramsOrOptionsOrHandler;
137
+ handler = maybeHandler;
138
+ }
139
+ else {
140
+ // Overload 3: tool(name, options, handler)
141
+ const opts = paramsOrOptionsOrHandler;
142
+ description = opts.description ?? name;
143
+ inputSchema = opts.params;
144
+ tier = opts.tier ?? "always";
145
+ tags = opts.tags;
146
+ gate = opts.gate;
147
+ timeout = opts.timeout;
148
+ handler = maybeHandler;
149
+ }
150
+ const toolConfig = {
151
+ name,
152
+ description,
153
+ inputSchema,
154
+ tier,
155
+ tags,
156
+ gate,
157
+ timeout,
158
+ handler,
159
+ };
160
+ this.#tools.set(name, toolConfig);
161
+ return this;
162
+ }
163
+ // ── Search ──────────────────────────────────────────────────────
164
+ searchTools(query, maxResults) {
165
+ return this.#searchIndex.search(query, maxResults ?? 10);
166
+ }
167
+ getVisibleTools(session) {
168
+ return getVisibleTools(this.#tools, session);
169
+ }
170
+ // ── Internal: Disclosure Mode Inference ─────────────────────────
171
+ #inferDisclosureMode() {
172
+ // If explicitly configured, use that
173
+ if (this.#config.disclosure)
174
+ return this.#config.disclosure;
175
+ // Auto-infer: if any tool uses non-default tiers, go progressive
176
+ for (const tool of this.#tools.values()) {
177
+ if (tool.tier === "discoverable" || tool.tier === "hidden") {
178
+ return { mode: "progressive" };
179
+ }
180
+ }
181
+ return { mode: "all" };
182
+ }
183
+ // ── Internal: Build MCP Server ──────────────────────────────────
184
+ #buildSdkServer() {
185
+ const mcp = new SdkMcpServer({
186
+ name: this.#config.name,
187
+ version: this.#config.version,
188
+ });
189
+ this.#searchIndex.build([...this.#tools.values()]);
190
+ const disclosure = this.#inferDisclosureMode();
191
+ if (disclosure.mode === "all") {
192
+ for (const tool of this.#tools.values()) {
193
+ this.#registerToolWithSdk(mcp, tool);
194
+ }
195
+ }
196
+ else if (disclosure.mode === "code-first") {
197
+ // Code-first: only meta-tools visible, all tools available via code
198
+ this.#registerSearchTool(mcp);
199
+ this.#registerCodeModeTool(mcp);
200
+ }
201
+ else {
202
+ // Progressive: always-tier + session-promoted tools + search_tools + optional code mode
203
+ const sessionVisible = this.#activeSession?.visibleTools;
204
+ for (const tool of this.#tools.values()) {
205
+ if (tool.tier === "always" || sessionVisible?.has(tool.name)) {
206
+ this.#registerToolWithSdk(mcp, tool);
207
+ }
208
+ }
209
+ this.#registerSearchTool(mcp);
210
+ if (this.#codeMode.enabled) {
211
+ this.#registerCodeModeTool(mcp);
212
+ }
213
+ }
214
+ this.#sdkServer = mcp;
215
+ return mcp;
216
+ }
217
+ #registerToolWithSdk(mcp, tool) {
218
+ const inputSchema = tool.inputSchema ? this.#zodToMcpShape(tool.inputSchema) : undefined;
219
+ const config = { description: tool.description };
220
+ if (inputSchema) {
221
+ config.inputSchema = inputSchema;
222
+ }
223
+ mcp.registerTool(tool.name, config, async (params) => {
224
+ return this.#executeToolHandler(tool, params, this.#activeAuth, this.#activeSession);
225
+ });
226
+ }
227
+ #zodToMcpShape(schema) {
228
+ if ("shape" in schema && typeof schema.shape === "object" && schema.shape !== null) {
229
+ return schema.shape;
230
+ }
231
+ return schema;
232
+ }
233
+ async #executeToolHandler(tool, params, auth, session) {
234
+ const effectiveAuth = auth ?? session?.auth ?? UNAUTHED;
235
+ const effectiveSession = session ?? createSession("default", effectiveAuth);
236
+ // Gate check — uses real auth context
237
+ if (tool.gate) {
238
+ const gateCtx = {
239
+ auth: effectiveAuth,
240
+ toolName: tool.name,
241
+ sessionId: effectiveSession.id,
242
+ metadata: { unlockedGates: effectiveSession.unlockedGates },
243
+ };
244
+ const gateResult = await tool.gate(gateCtx);
245
+ if (!gateResult.allowed) {
246
+ this.#events.emit("gate:denied", {
247
+ toolName: tool.name,
248
+ reason: gateResult.reason,
249
+ auth: effectiveAuth,
250
+ sessionId: effectiveSession.id,
251
+ timestamp: Date.now(),
252
+ });
253
+ return gateDenialResult(gateResult.reason, gateResult.hint);
254
+ }
255
+ }
256
+ const ctx = {
257
+ session: effectiveSession,
258
+ auth: effectiveAuth,
259
+ };
260
+ const timeoutMs = tool.timeout;
261
+ const startTime = Date.now();
262
+ this.#events.emit("tool:call", {
263
+ toolName: tool.name,
264
+ params,
265
+ auth: effectiveAuth,
266
+ sessionId: effectiveSession.id,
267
+ timestamp: startTime,
268
+ });
269
+ try {
270
+ // Run handler with AsyncLocalStorage context so getAuthContext()/getSession() work
271
+ let raw;
272
+ const runHandler = () => {
273
+ if (timeoutMs) {
274
+ return Promise.race([
275
+ tool.handler(params, ctx),
276
+ new Promise((_, reject) => setTimeout(() => reject(new ToolTimeoutError(tool.name, timeoutMs)), timeoutMs)),
277
+ ]);
278
+ }
279
+ return tool.handler(params, ctx);
280
+ };
281
+ raw = await withContext({ auth: effectiveAuth, session: effectiveSession }, runHandler);
282
+ const durationMs = Date.now() - startTime;
283
+ // Record tool call in session history
284
+ effectiveSession.toolCallHistory.push({
285
+ toolName: tool.name,
286
+ params,
287
+ timestamp: startTime,
288
+ durationMs,
289
+ });
290
+ this.#events.emit("tool:result", {
291
+ toolName: tool.name,
292
+ durationMs,
293
+ isError: false,
294
+ auth: effectiveAuth,
295
+ sessionId: effectiveSession.id,
296
+ timestamp: Date.now(),
297
+ });
298
+ // Trigger gate unlocks: if this tool is a prerequisite for hidden tools
299
+ const promoted = handleGateUnlock(this.#tools, effectiveSession, tool.name);
300
+ if (promoted.length > 0 && this.#sdkServer) {
301
+ for (const name of promoted) {
302
+ const promotedTool = this.#tools.get(name);
303
+ if (promotedTool) {
304
+ this.#registerToolWithSdk(this.#sdkServer, promotedTool);
305
+ }
306
+ }
307
+ this.#sdkServer.sendToolListChanged();
308
+ this.#events.emit("tools:unlocked", {
309
+ toolNames: promoted,
310
+ prerequisite: tool.name,
311
+ sessionId: effectiveSession.id,
312
+ timestamp: Date.now(),
313
+ });
314
+ }
315
+ return normalizeToolResult(raw);
316
+ }
317
+ catch (err) {
318
+ const durationMs = Date.now() - startTime;
319
+ effectiveSession.toolCallHistory.push({
320
+ toolName: tool.name,
321
+ params,
322
+ timestamp: startTime,
323
+ durationMs,
324
+ });
325
+ this.#events.emit("tool:result", {
326
+ toolName: tool.name,
327
+ durationMs,
328
+ isError: true,
329
+ auth: effectiveAuth,
330
+ sessionId: effectiveSession.id,
331
+ timestamp: Date.now(),
332
+ });
333
+ if (err instanceof ToolTimeoutError) {
334
+ return errorResult(`Tool "${tool.name}" timed out after ${timeoutMs}ms`);
335
+ }
336
+ const message = err instanceof Error ? err.message : String(err);
337
+ return errorResult(`Tool "${tool.name}" failed: ${message}`);
338
+ }
339
+ }
340
+ #registerSearchTool(mcp) {
341
+ mcp.registerTool("search_tools", {
342
+ description: "Search for available tools by query. Returns matching tools and makes them available for use.",
343
+ inputSchema: { query: z.string().describe("Search query to find relevant tools") },
344
+ }, async ({ query }) => {
345
+ const results = this.searchTools(query);
346
+ if (results.length === 0) {
347
+ return {
348
+ content: [{ type: "text", text: "No tools found matching your query." }],
349
+ };
350
+ }
351
+ let promoted = false;
352
+ for (const result of results) {
353
+ const tool = this.#tools.get(result.name);
354
+ if (tool && tool.tier !== "always") {
355
+ // Update session visibility
356
+ if (this.#activeSession) {
357
+ promoteToVisible(this.#activeSession, result.name);
358
+ }
359
+ // Register with MCP SDK so client can call it
360
+ if (this.#sdkServer) {
361
+ this.#registerToolWithSdk(this.#sdkServer, tool);
362
+ promoted = true;
363
+ }
364
+ }
365
+ }
366
+ if (promoted)
367
+ this.#sdkServer?.sendToolListChanged();
368
+ const lines = results.map((r) => `- **${r.name}** (score: ${r.score.toFixed(2)}): ${r.description}${r.tags?.length ? ` [${r.tags.join(", ")}]` : ""}`);
369
+ return {
370
+ content: [
371
+ {
372
+ type: "text",
373
+ text: `Found ${results.length} tool(s):\n${lines.join("\n")}`,
374
+ },
375
+ ],
376
+ };
377
+ });
378
+ }
379
+ #registerCodeModeTool(mcp) {
380
+ const allTools = [...this.#tools.values()];
381
+ const types = generateTypes(allTools);
382
+ const executorType = this.#codeMode.executor ?? "function";
383
+ const executor = executorType === "worker" ? new WorkerExecutor() : new FunctionExecutor();
384
+ const description = `Execute code to achieve a goal.\n\nAvailable:\n${types}\n\nWrite an async arrow function in JavaScript that returns the result.\nDo NOT use TypeScript syntax.\n\nExample: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`;
385
+ mcp.registerTool("execute_workflow", {
386
+ description,
387
+ inputSchema: { code: z.string().describe("JavaScript async arrow function to execute") },
388
+ }, async ({ code }) => {
389
+ const effectiveAuth = this.#activeAuth;
390
+ const effectiveSession = this.#activeSession ?? createSession("default", effectiveAuth);
391
+ // Pre-validation: extract tool references from code and validate gates upfront
392
+ const referencedTools = [];
393
+ for (const tool of allTools) {
394
+ const safeName = sanitizeToolName(tool.name);
395
+ if (code.includes(`codemode.${safeName}`)) {
396
+ referencedTools.push(tool);
397
+ }
398
+ }
399
+ for (const tool of referencedTools) {
400
+ if (tool.gate) {
401
+ const gateCtx = {
402
+ auth: effectiveAuth,
403
+ toolName: tool.name,
404
+ sessionId: effectiveSession.id,
405
+ metadata: { unlockedGates: effectiveSession.unlockedGates },
406
+ };
407
+ const gateResult = await tool.gate(gateCtx);
408
+ if (!gateResult.allowed) {
409
+ return {
410
+ content: [
411
+ {
412
+ type: "text",
413
+ text: JSON.stringify({
414
+ error: `Permission denied for tool '${tool.name}'`,
415
+ reason: gateResult.reason,
416
+ ...(gateResult.hint ? { hint: gateResult.hint } : {}),
417
+ }),
418
+ },
419
+ ],
420
+ isError: true,
421
+ };
422
+ }
423
+ }
424
+ }
425
+ const fns = {};
426
+ for (const tool of allTools) {
427
+ const safeName = sanitizeToolName(tool.name);
428
+ fns[safeName] = async (args) => {
429
+ const validated = tool.inputSchema ? tool.inputSchema.parse(args) : args;
430
+ const ctx = { session: effectiveSession, auth: effectiveAuth };
431
+ const result = normalizeToolResult(await tool.handler(validated, ctx));
432
+ const textPart = result.content.find((c) => c.type === "text");
433
+ if (textPart && textPart.type === "text") {
434
+ try {
435
+ return JSON.parse(textPart.text);
436
+ }
437
+ catch {
438
+ return textPart.text;
439
+ }
440
+ }
441
+ return result.structuredContent ?? null;
442
+ };
443
+ }
444
+ const normalizedCode = normalizeCode(code);
445
+ const execResult = await executor.execute(normalizedCode, fns);
446
+ if (execResult.error) {
447
+ const logCtx = execResult.logs?.length
448
+ ? `\n\nConsole output:\n${execResult.logs.join("\n")}`
449
+ : "";
450
+ return {
451
+ content: [
452
+ {
453
+ type: "text",
454
+ text: `Code execution failed: ${execResult.error}${logCtx}`,
455
+ },
456
+ ],
457
+ isError: true,
458
+ };
459
+ }
460
+ const output = {
461
+ code,
462
+ result: execResult.result,
463
+ ...(execResult.logs?.length ? { logs: execResult.logs } : {}),
464
+ };
465
+ return { content: [{ type: "text", text: JSON.stringify(output) }] };
466
+ });
467
+ }
468
+ // ── Transport: stdio ────────────────────────────────────────────
469
+ /**
470
+ * Connect to stdio transport. For local dev with Claude Desktop.
471
+ */
472
+ async stdio() {
473
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
474
+ const mcp = this.#buildSdkServer();
475
+ const transport = new StdioServerTransport();
476
+ await mcp.connect(transport);
477
+ }
478
+ // ── Transport: Lambda ───────────────────────────────────────────
479
+ /**
480
+ * Create a Lambda handler for Function URLs or API Gateway v2.
481
+ * Uses WebStandard transport: Request in → Response out.
482
+ * Stateless per-request MCP server isolation.
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * // Lambda Function URL handler
487
+ * export default server.lambda();
488
+ *
489
+ * // Or with config
490
+ * export const handler = server.lambda({
491
+ * baseUrl: "https://abc.lambda-url.us-east-1.on.aws",
492
+ * });
493
+ * ```
494
+ */
495
+ lambda(config) {
496
+ this.#buildSdkServer(); // validate registration
497
+ const sessionStore = config?.sessionStore ?? new MemorySessionStore();
498
+ const authProvider = this.#authProvider;
499
+ const baseUrl = config?.baseUrl;
500
+ const resourceMetadata = config?.resourceMetadata;
501
+ return async (event) => {
502
+ const apiEvent = event;
503
+ const headers = apiEvent.headers ?? {};
504
+ const method = apiEvent.httpMethod ?? apiEvent.requestContext?.http?.method ?? "POST";
505
+ const path = apiEvent.rawPath ?? apiEvent.path ?? "/";
506
+ // RFC 9728 metadata endpoint
507
+ if (path.endsWith("/.well-known/oauth-protected-resource") && resourceMetadata) {
508
+ return {
509
+ statusCode: 200,
510
+ headers: { "Content-Type": "application/json" },
511
+ body: JSON.stringify(buildResourceMetadata(resourceMetadata)),
512
+ };
513
+ }
514
+ // Auth
515
+ let auth = UNAUTHED;
516
+ if (authProvider) {
517
+ auth = await authProvider.authenticate({
518
+ headers: headers,
519
+ });
520
+ if (!auth.authenticated) {
521
+ const wwwAuth = baseUrl
522
+ ? buildWwwAuthenticateHeader(baseUrl)
523
+ : 'Bearer realm="mcp-server"';
524
+ return {
525
+ statusCode: 401,
526
+ headers: { "Content-Type": "application/json", "WWW-Authenticate": wwwAuth },
527
+ body: JSON.stringify({
528
+ jsonrpc: "2.0",
529
+ error: { code: -32000, message: "Authentication required" },
530
+ id: null,
531
+ }),
532
+ };
533
+ }
534
+ }
535
+ if (method !== "POST") {
536
+ return {
537
+ statusCode: 405,
538
+ headers: { Allow: "POST" },
539
+ body: JSON.stringify({ error: "Method not allowed" }),
540
+ };
541
+ }
542
+ // Session
543
+ const requestId = apiEvent.requestContext?.requestId ?? crypto.randomUUID();
544
+ const sessionId = headers["x-session-id"] ?? requestId;
545
+ let session = await sessionStore.get(sessionId);
546
+ if (!session)
547
+ session = createSession(sessionId, auth);
548
+ session.auth = auth;
549
+ session.lastAccessedAt = Date.now();
550
+ await sessionStore.set(session);
551
+ // Set active context so tool handlers can access session/auth
552
+ this.#activeSession = session;
553
+ this.#activeAuth = auth;
554
+ // Build a fresh MCP server + WebStandard transport per request
555
+ const mcp = this.#buildSdkServer();
556
+ const { WebStandardStreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
557
+ const transport = new WebStandardStreamableHTTPServerTransport({
558
+ sessionIdGenerator: undefined, // stateless
559
+ enableJsonResponse: true, // JSON responses, no SSE (Lambda can't stream)
560
+ });
561
+ await mcp.connect(transport);
562
+ // Convert Lambda event → Web Standard Request
563
+ const bodyStr = apiEvent.isBase64Encoded && apiEvent.body
564
+ ? Buffer.from(apiEvent.body, "base64").toString("utf-8")
565
+ : (apiEvent.body ?? "");
566
+ const url = `https://localhost${path}`;
567
+ const reqHeaders = new Headers();
568
+ for (const [k, v] of Object.entries(headers)) {
569
+ if (v)
570
+ reqHeaders.set(k, String(v));
571
+ }
572
+ reqHeaders.set("content-type", "application/json");
573
+ // MCP protocol requires Accept header with both content types
574
+ if (!reqHeaders.has("accept")) {
575
+ reqHeaders.set("accept", "application/json, text/event-stream");
576
+ }
577
+ const webRequest = new Request(url, {
578
+ method: "POST",
579
+ headers: reqHeaders,
580
+ body: bodyStr,
581
+ });
582
+ // Process through MCP transport → get Web Standard Response
583
+ const webResponse = await transport.handleRequest(webRequest);
584
+ // Convert Web Standard Response → Lambda response
585
+ const responseBody = await webResponse.text();
586
+ const responseHeaders = {
587
+ "x-session-id": sessionId,
588
+ };
589
+ webResponse.headers.forEach((v, k) => {
590
+ responseHeaders[k] = v;
591
+ });
592
+ await transport.close();
593
+ await mcp.close();
594
+ // Persist session changes (promoted tools, gate unlocks, call history)
595
+ await sessionStore.set(session);
596
+ return {
597
+ statusCode: webResponse.status,
598
+ headers: responseHeaders,
599
+ body: responseBody,
600
+ };
601
+ };
602
+ }
603
+ // ── Transport: Express ──────────────────────────────────────────
604
+ /**
605
+ * Create Express route handlers. Call `setup(app)` to mount.
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * import express from "express";
610
+ * const app = express();
611
+ * app.use(express.json());
612
+ * server.express().setup(app);
613
+ * app.listen(3000);
614
+ * ```
615
+ */
616
+ express(config) {
617
+ const authProvider = this.#authProvider;
618
+ const baseUrl = config?.baseUrl;
619
+ const resourceMetadata = config?.resourceMetadata;
620
+ const mcpPath = config?.mcpPath ?? "/mcp";
621
+ const authMiddleware = async (req, res, next) => {
622
+ if (!authProvider) {
623
+ next();
624
+ return;
625
+ }
626
+ const auth = await authProvider.authenticate({ headers: req.headers });
627
+ if (!auth.authenticated) {
628
+ const wwwAuth = baseUrl ? buildWwwAuthenticateHeader(baseUrl) : 'Bearer realm="mcp-server"';
629
+ res
630
+ .status(401)
631
+ .set({ "WWW-Authenticate": wwwAuth })
632
+ .json({
633
+ jsonrpc: "2.0",
634
+ error: { code: -32000, message: "Authentication required" },
635
+ id: null,
636
+ });
637
+ return;
638
+ }
639
+ next();
640
+ };
641
+ const setup = (app) => {
642
+ const e = app;
643
+ if (resourceMetadata) {
644
+ e.get("/.well-known/oauth-protected-resource", ((_req, res) => {
645
+ res.json(buildResourceMetadata(resourceMetadata));
646
+ }));
647
+ }
648
+ e.post(mcpPath, authMiddleware, (async (req, res) => {
649
+ const mcp = this.#buildSdkServer();
650
+ const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
651
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
652
+ res.on("close", () => {
653
+ transport.close();
654
+ mcp.close();
655
+ });
656
+ await mcp.connect(transport);
657
+ await transport.handleRequest(req, res, req.body);
658
+ }));
659
+ const methodNotAllowed = ((_req, res) => {
660
+ res
661
+ .status(405)
662
+ .json({ error: "Method not allowed" });
663
+ });
664
+ e.get(mcpPath, methodNotAllowed);
665
+ e.delete(mcpPath, methodNotAllowed);
666
+ };
667
+ return { setup };
668
+ }
669
+ // ── Transport: Bun.serve ────────────────────────────────────────
670
+ /**
671
+ * Start a Bun HTTP server. Zero dependencies — uses Bun.serve() natively.
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * server.bun({ port: 3000 });
676
+ * // MCP server running at http://localhost:3000/mcp
677
+ * ```
678
+ */
679
+ bun(config) {
680
+ const port = config?.port ?? 3000;
681
+ const hostname = config?.hostname ?? "localhost";
682
+ const mcpPath = config?.mcpPath ?? "/mcp";
683
+ const authProvider = this.#authProvider;
684
+ const BunRuntime = globalThis.Bun;
685
+ if (!BunRuntime?.serve) {
686
+ throw new Error("server.bun() requires the Bun runtime. Use server.express() or server.lambda() for Node.js.");
687
+ }
688
+ const bunServer = BunRuntime.serve({
689
+ port,
690
+ hostname,
691
+ fetch: async (req) => {
692
+ const url = new URL(req.url);
693
+ if (req.method !== "POST" || url.pathname !== mcpPath) {
694
+ return new Response(JSON.stringify({ error: "Use POST " + mcpPath }), {
695
+ status: req.method !== "POST" ? 405 : 404,
696
+ headers: { "Content-Type": "application/json" },
697
+ });
698
+ }
699
+ // Auth
700
+ if (authProvider) {
701
+ const headers = {};
702
+ req.headers.forEach((v, k) => {
703
+ headers[k] = v;
704
+ });
705
+ const auth = await authProvider.authenticate({ headers });
706
+ if (!auth.authenticated) {
707
+ return new Response(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Authentication required" }, id: null }), { status: 401, headers: { "Content-Type": "application/json" } });
708
+ }
709
+ }
710
+ // Stateless MCP handler
711
+ const mcp = this.#buildSdkServer();
712
+ const { WebStandardStreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
713
+ const transport = new WebStandardStreamableHTTPServerTransport({
714
+ sessionIdGenerator: undefined,
715
+ enableJsonResponse: true,
716
+ });
717
+ await mcp.connect(transport);
718
+ const response = await transport.handleRequest(req);
719
+ await transport.close();
720
+ await mcp.close();
721
+ return response;
722
+ },
723
+ });
724
+ return {
725
+ stop: () => bunServer.stop(),
726
+ url: `http://${hostname}:${port}${mcpPath}`,
727
+ };
728
+ }
729
+ // ── Accessors ───────────────────────────────────────────────────
730
+ get config() {
731
+ return this.#config;
732
+ }
733
+ get toolCount() {
734
+ return this.#tools.size;
735
+ }
736
+ getToolConfig(name) {
737
+ return this.#tools.get(name);
738
+ }
739
+ // ── Deploy ──────────────────────────────────────────────────────
740
+ /**
741
+ * Deploy this server to AWS Lambda with a Function URL.
742
+ * Returns the live HTTPS endpoint URL.
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * const server = new McpServer("my-tools@1.0.0")
747
+ * .tool("ping", async () => "pong");
748
+ *
749
+ * const { url } = await server.deploy({ entry: "./src/server.ts" });
750
+ * console.log(`Live at: ${url}`);
751
+ * ```
752
+ */
753
+ async deploy(config) {
754
+ const { deploy } = await import("./deploy.js");
755
+ return deploy({
756
+ ...config,
757
+ functionName: config.functionName ?? this.#config.name,
758
+ });
759
+ }
760
+ /**
761
+ * Destroy a previously deployed Lambda function.
762
+ */
763
+ async destroy(functionName, region) {
764
+ const { destroy } = await import("./deploy.js");
765
+ return destroy(functionName ?? this.#config.name, region);
766
+ }
767
+ }
768
+ //# sourceMappingURL=server.js.map