@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.
- package/README.md +610 -0
- package/dist/auth/api-key.d.ts +29 -0
- package/dist/auth/api-key.d.ts.map +1 -0
- package/dist/auth/api-key.js +42 -0
- package/dist/auth/api-key.js.map +1 -0
- package/dist/auth/cognito.d.ts +26 -0
- package/dist/auth/cognito.d.ts.map +1 -0
- package/dist/auth/cognito.js +58 -0
- package/dist/auth/cognito.js.map +1 -0
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +21 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/metadata.d.ts +30 -0
- package/dist/auth/metadata.d.ts.map +1 -0
- package/dist/auth/metadata.js +25 -0
- package/dist/auth/metadata.js.map +1 -0
- package/dist/auth/types.d.ts +8 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/codemode/executor.d.ts +29 -0
- package/dist/codemode/executor.d.ts.map +1 -0
- package/dist/codemode/executor.js +154 -0
- package/dist/codemode/executor.js.map +1 -0
- package/dist/codemode/index.d.ts +4 -0
- package/dist/codemode/index.d.ts.map +1 -0
- package/dist/codemode/index.js +3 -0
- package/dist/codemode/index.js.map +1 -0
- package/dist/codemode/types.d.ts +10 -0
- package/dist/codemode/types.d.ts.map +1 -0
- package/dist/codemode/types.js +195 -0
- package/dist/codemode/types.js.map +1 -0
- package/dist/compose.d.ts +57 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +138 -0
- package/dist/compose.js.map +1 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +39 -0
- package/dist/context.js.map +1 -0
- package/dist/deploy.d.ts +57 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +281 -0
- package/dist/deploy.js.map +1 -0
- package/dist/disclosure/index.d.ts +3 -0
- package/dist/disclosure/index.d.ts.map +1 -0
- package/dist/disclosure/index.js +3 -0
- package/dist/disclosure/index.js.map +1 -0
- package/dist/disclosure/search.d.ts +15 -0
- package/dist/disclosure/search.d.ts.map +1 -0
- package/dist/disclosure/search.js +73 -0
- package/dist/disclosure/search.js.map +1 -0
- package/dist/disclosure/tier.d.ts +16 -0
- package/dist/disclosure/tier.d.ts.map +1 -0
- package/dist/disclosure/tier.js +69 -0
- package/dist/disclosure/tier.js.map +1 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +93 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +28 -0
- package/dist/events.js.map +1 -0
- package/dist/gating/combinators.d.ts +10 -0
- package/dist/gating/combinators.d.ts.map +1 -0
- package/dist/gating/combinators.js +34 -0
- package/dist/gating/combinators.js.map +1 -0
- package/dist/gating/gates.d.ts +21 -0
- package/dist/gating/gates.d.ts.map +1 -0
- package/dist/gating/gates.js +74 -0
- package/dist/gating/gates.js.map +1 -0
- package/dist/gating/index.d.ts +3 -0
- package/dist/gating/index.d.ts.map +1 -0
- package/dist/gating/index.js +3 -0
- package/dist/gating/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +161 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +768 -0
- package/dist/server.js.map +1 -0
- package/dist/session/dynamo.d.ts +23 -0
- package/dist/session/dynamo.d.ts.map +1 -0
- package/dist/session/dynamo.js +92 -0
- package/dist/session/dynamo.js.map +1 -0
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +4 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/memory.d.ts +16 -0
- package/dist/session/memory.d.ts.map +1 -0
- package/dist/session/memory.js +43 -0
- package/dist/session/memory.js.map +1 -0
- package/dist/session/sqlite.d.ts +17 -0
- package/dist/session/sqlite.d.ts.map +1 -0
- package/dist/session/sqlite.js +79 -0
- package/dist/session/sqlite.js.map +1 -0
- package/dist/ssrf.d.ts +25 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +88 -0
- package/dist/ssrf.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- 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
|