@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/README.md
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# @usestratus/mcp-aws
|
|
2
|
+
|
|
3
|
+
Build, deploy, and manage MCP servers on AWS in TypeScript. Progressive disclosure, tool gating, code mode, and one-line Lambda deploys.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { McpServer, apiKey, role, deploy } from "@usestratus/mcp-aws";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
const server = new McpServer("my-tools@1.0.0")
|
|
10
|
+
.auth(apiKey({ "sk-live-xxx": { roles: ["admin"] } }))
|
|
11
|
+
.tool("greet", z.object({ name: z.string() }), async ({ name }) => {
|
|
12
|
+
return `Hello, ${name}!`;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const handler = server.lambda();
|
|
16
|
+
|
|
17
|
+
// Or deploy directly from code:
|
|
18
|
+
// const { url } = await server.deploy({ entry: "./src/server.ts" });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add @usestratus/mcp-aws zod
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### 5 lines to a working MCP server
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { McpServer } from "@usestratus/mcp-aws";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
const server = new McpServer("my-server@1.0.0");
|
|
36
|
+
server.tool("greet", z.object({ name: z.string() }), async ({ name }) => `Hello, ${name}!`);
|
|
37
|
+
export const handler = server.lambda();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Local development with Claude Desktop
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
await server.stdio();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Deploy to AWS Lambda
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const { url } = await server.deploy({ entry: "./src/server.ts" });
|
|
50
|
+
// → https://xxx.lambda-url.us-east-1.on.aws/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## API Reference
|
|
56
|
+
|
|
57
|
+
### McpServer
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// String constructor
|
|
61
|
+
const server = new McpServer("my-server@1.0.0");
|
|
62
|
+
|
|
63
|
+
// Config object (for advanced options)
|
|
64
|
+
const server = new McpServer({
|
|
65
|
+
name: "my-server",
|
|
66
|
+
version: "1.0.0",
|
|
67
|
+
codeMode: { enabled: true, executor: "worker" },
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Tool Registration
|
|
72
|
+
|
|
73
|
+
Three overloads — pick the one that fits:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// Simple: name + handler (no params)
|
|
77
|
+
server.tool("ping", async () => "pong");
|
|
78
|
+
|
|
79
|
+
// With params: name + Zod schema + handler
|
|
80
|
+
server.tool("greet", z.object({ name: z.string() }), async ({ name }) => {
|
|
81
|
+
return `Hello, ${name}!`;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Full config: name + options + handler
|
|
85
|
+
server.tool("admin_action", {
|
|
86
|
+
description: "Reset a user account",
|
|
87
|
+
params: z.object({ userId: z.string() }),
|
|
88
|
+
tier: "hidden",
|
|
89
|
+
gate: role("admin"),
|
|
90
|
+
timeout: 5000,
|
|
91
|
+
tags: ["admin"],
|
|
92
|
+
}, async ({ userId }) => {
|
|
93
|
+
return { reset: true, userId };
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Return values are auto-coerced:**
|
|
98
|
+
- `string` → text content
|
|
99
|
+
- `object` / `array` → JSON-serialized text content
|
|
100
|
+
- `undefined` → empty content
|
|
101
|
+
- `ToolResult` → pass-through (for full control)
|
|
102
|
+
|
|
103
|
+
### Method Chaining
|
|
104
|
+
|
|
105
|
+
Everything returns `this`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const server = new McpServer("my-server@1.0.0")
|
|
109
|
+
.auth(apiKey({ "sk-123": { roles: ["admin"] } }))
|
|
110
|
+
.tool("ping", async () => "pong")
|
|
111
|
+
.tool("greet", z.object({ name: z.string() }), async ({ name }) => `Hello, ${name}!`)
|
|
112
|
+
.on("tool:call", (e) => console.log(e.toolName));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Auth
|
|
118
|
+
|
|
119
|
+
Configure once on the server. All transports inherit it.
|
|
120
|
+
|
|
121
|
+
### API Key
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { apiKey } from "@usestratus/mcp-aws";
|
|
125
|
+
|
|
126
|
+
server.auth(apiKey({
|
|
127
|
+
"sk-live-abc123": { subject: "user-1", roles: ["admin"] },
|
|
128
|
+
"sk-live-xyz789": { subject: "user-2", roles: ["reader"] },
|
|
129
|
+
}));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Cognito JWT
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { cognito } from "@usestratus/mcp-aws";
|
|
136
|
+
|
|
137
|
+
server.auth(cognito({
|
|
138
|
+
userPoolId: "us-east-1_abc123",
|
|
139
|
+
region: "us-east-1",
|
|
140
|
+
audience: "my-client-id", // optional
|
|
141
|
+
}));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Chain Multiple Providers
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
server.auth(
|
|
148
|
+
apiKey({ "sk-123": { roles: ["admin"] } }),
|
|
149
|
+
cognito({ userPoolId: "...", region: "us-east-1" }),
|
|
150
|
+
);
|
|
151
|
+
// Tries each in order, returns first success
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### AsyncLocalStorage Context
|
|
155
|
+
|
|
156
|
+
Tool handlers can access auth without explicit parameters:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { getAuthContext, getSession } from "@usestratus/mcp-aws";
|
|
160
|
+
|
|
161
|
+
server.tool("my_tool", async () => {
|
|
162
|
+
const auth = getAuthContext(); // works anywhere in the call stack
|
|
163
|
+
const session = getSession();
|
|
164
|
+
return `Hello, ${auth.subject}!`;
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Progressive Disclosure
|
|
171
|
+
|
|
172
|
+
59 tools? No problem. Only show what matters.
|
|
173
|
+
|
|
174
|
+
### Tier System
|
|
175
|
+
|
|
176
|
+
| Tier | Behavior | Default? |
|
|
177
|
+
|------|----------|----------|
|
|
178
|
+
| `always` | Always in `tools/list`. The server's front door. | Yes |
|
|
179
|
+
| `discoverable` | Found via `search_tools`. Promoted on discovery. | |
|
|
180
|
+
| `hidden` | Invisible until a gate unlocks it. | |
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
server
|
|
184
|
+
.tool("get_weather", async () => "sunny") // always (default)
|
|
185
|
+
.tool("get_forecast", { tier: "discoverable", tags: ["weather"] }, handler) // searchable
|
|
186
|
+
.tool("delete_account", { tier: "hidden", gate: requires("confirm") }, handler) // gated
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### How It Works
|
|
190
|
+
|
|
191
|
+
1. Client calls `tools/list` → gets only `always` tier tools + `search_tools`
|
|
192
|
+
2. Agent calls `search_tools("weather forecast")` → BM25 search finds matches
|
|
193
|
+
3. Matches promoted to session → server sends `tools/list_changed`
|
|
194
|
+
4. Client re-fetches → now sees promoted tools
|
|
195
|
+
5. Visibility is per-session, persisted in session store
|
|
196
|
+
|
|
197
|
+
### Disclosure Modes
|
|
198
|
+
|
|
199
|
+
Auto-inferred from your tool tiers. Override with config:
|
|
200
|
+
|
|
201
|
+
| Mode | When | `tools/list` shows |
|
|
202
|
+
|------|------|--------------------|
|
|
203
|
+
| `all` | All tools are `always` | Everything |
|
|
204
|
+
| `progressive` | Any `discoverable`/`hidden` | `always` + `search_tools` |
|
|
205
|
+
| `code-first` | Explicit config | `search_tools` + `execute_workflow` only |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Tool Gating
|
|
210
|
+
|
|
211
|
+
Access control at the tool level.
|
|
212
|
+
|
|
213
|
+
### Role-Based
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { role } from "@usestratus/mcp-aws";
|
|
217
|
+
|
|
218
|
+
server.tool("admin_action", { gate: role("admin") }, handler);
|
|
219
|
+
server.tool("write_action", { gate: role("admin", "editor") }, handler); // any of these roles
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Prerequisite (Workflow Enforcement)
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { requires } from "@usestratus/mcp-aws";
|
|
226
|
+
|
|
227
|
+
server
|
|
228
|
+
.tool("review_trade", handler)
|
|
229
|
+
.tool("execute_trade", { tier: "hidden", gate: requires("review_trade") }, handler);
|
|
230
|
+
// execute_trade is invisible until review_trade is called
|
|
231
|
+
// Then it auto-promotes and tools/list_changed fires
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Dynamic Check
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { check } from "@usestratus/mcp-aws";
|
|
238
|
+
|
|
239
|
+
server.tool("update_deal", {
|
|
240
|
+
gate: check((ctx) => ctx.auth.claims.org === "acme", "Wrong org"),
|
|
241
|
+
}, handler);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Rate Limiting
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { rateLimit } from "@usestratus/mcp-aws";
|
|
248
|
+
|
|
249
|
+
server.tool("expensive_op", {
|
|
250
|
+
gate: rateLimit({ max: 10, windowMs: 60_000 }),
|
|
251
|
+
}, handler);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Composite Gates
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { all, any, role, requires, rateLimit } from "@usestratus/mcp-aws";
|
|
258
|
+
|
|
259
|
+
server.tool("approve_discount", {
|
|
260
|
+
gate: all(
|
|
261
|
+
role("sales-manager"),
|
|
262
|
+
requires("review_discount"),
|
|
263
|
+
rateLimit({ max: 10, windowMs: 3_600_000 }),
|
|
264
|
+
),
|
|
265
|
+
}, handler);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Gate Denial
|
|
269
|
+
|
|
270
|
+
When a gate blocks, the agent gets a structured error it can self-correct from:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"error": "Permission denied",
|
|
275
|
+
"reason": "Requires \"review_trade\" to be called first",
|
|
276
|
+
"hint": "Call the \"review_trade\" tool before using \"execute_trade\"."
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Code Mode
|
|
283
|
+
|
|
284
|
+
Reduce N tool calls to 1. The agent writes code that orchestrates tools.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const server = new McpServer({
|
|
288
|
+
name: "tools",
|
|
289
|
+
version: "1.0.0",
|
|
290
|
+
codeMode: { enabled: true, executor: "worker" },
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
This registers an `execute_workflow` tool. The agent:
|
|
295
|
+
1. Calls `search_tools` to discover available tools + type signatures
|
|
296
|
+
2. Writes an async arrow function using `codemode.toolName(args)`
|
|
297
|
+
3. Calls `execute_workflow` with the code
|
|
298
|
+
4. Server validates gates, executes in isolated V8, returns result
|
|
299
|
+
|
|
300
|
+
### Wrap Any Existing MCP Server
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { codeMcpServer } from "@usestratus/mcp-aws";
|
|
304
|
+
|
|
305
|
+
const codeServer = await codeMcpServer({ server: existingMcpServer });
|
|
306
|
+
// → codeServer has one tool: execute_code
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Transports
|
|
312
|
+
|
|
313
|
+
### Lambda (Serverless)
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
export const handler = server.lambda();
|
|
317
|
+
|
|
318
|
+
// With session store
|
|
319
|
+
import { DynamoSessionStore } from "@usestratus/mcp-aws/dynamo";
|
|
320
|
+
|
|
321
|
+
export const handler = server.lambda({
|
|
322
|
+
sessionStore: new DynamoSessionStore({ tableName: "mcp-sessions" }),
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Express (Container/ECS/EC2)
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import express from "express";
|
|
330
|
+
|
|
331
|
+
const app = express();
|
|
332
|
+
app.use(express.json());
|
|
333
|
+
server.express({ mcpPath: "/mcp" }).setup(app);
|
|
334
|
+
app.listen(3000);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Bun.serve (Zero Dependencies)
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const { url, stop } = server.bun({ port: 3000 });
|
|
341
|
+
// MCP server at http://localhost:3000/mcp
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Native `Bun.serve()` — no Express, no dependencies. Handles auth, routes, and cleanup.
|
|
345
|
+
|
|
346
|
+
### Stdio (Claude Desktop)
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
await server.stdio();
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Stateless Handler (Bun/Deno/Any Runtime)
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
import { createMcpHandler } from "@usestratus/mcp-aws";
|
|
356
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
357
|
+
|
|
358
|
+
const handler = createMcpHandler({ server: myMcpServer });
|
|
359
|
+
Bun.serve({ fetch: handler });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Deploy
|
|
365
|
+
|
|
366
|
+
### One-Line Deploy
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const { url } = await server.deploy({ entry: "./src/server.ts" });
|
|
370
|
+
console.log(url); // https://xxx.lambda-url.us-east-1.on.aws/
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Deploy with Options
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
const { url, functionName, functionArn } = await server.deploy({
|
|
377
|
+
entry: "./src/server.ts",
|
|
378
|
+
region: "us-east-1",
|
|
379
|
+
functionName: "my-mcp-server",
|
|
380
|
+
memory: 512,
|
|
381
|
+
timeout: 30,
|
|
382
|
+
environment: { DATABASE_URL: "postgres://..." },
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Destroy
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
await server.destroy();
|
|
390
|
+
// or
|
|
391
|
+
await server.destroy("custom-function-name", "us-east-1");
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Security Modes
|
|
395
|
+
|
|
396
|
+
| Mode | `urlAuth` | `vpc` | Who can call |
|
|
397
|
+
|------|-----------|-------|-------------|
|
|
398
|
+
| Public (default) | `"NONE"` | — | Anyone + MCP-level auth |
|
|
399
|
+
| IAM-signed | `"AWS_IAM"` | — | AWS services with SigV4 |
|
|
400
|
+
| VPC-only | `"none"` | set | Resources in the VPC |
|
|
401
|
+
| VPC + IAM | `"AWS_IAM"` | set | VPC with SigV4 |
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// Private deployment inside a VPC
|
|
405
|
+
await server.deploy({
|
|
406
|
+
entry: "./src/server.ts",
|
|
407
|
+
urlAuth: "none", // no public URL
|
|
408
|
+
vpc: {
|
|
409
|
+
subnetIds: ["subnet-abc123", "subnet-def456"],
|
|
410
|
+
securityGroupIds: ["sg-xyz789"],
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## SSRF Protection
|
|
418
|
+
|
|
419
|
+
Prevent tools from accessing internal infrastructure:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { isBlockedUrl, assertSafeUrl } from "@usestratus/mcp-aws";
|
|
423
|
+
|
|
424
|
+
server.tool("fetch", z.object({ url: z.string() }), async ({ url }) => {
|
|
425
|
+
assertSafeUrl(url); // throws if private IP, metadata endpoint, etc.
|
|
426
|
+
return (await fetch(url)).text();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Or check manually
|
|
430
|
+
if (isBlockedUrl(url)) return "Blocked";
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Blocks: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.169.254` (AWS metadata), `::1`, link-local, non-HTTP schemes.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Observability
|
|
438
|
+
|
|
439
|
+
Typed event system for monitoring:
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
server
|
|
443
|
+
.on("tool:call", (e) => {
|
|
444
|
+
console.log(`${e.toolName} called by ${e.auth.subject}`);
|
|
445
|
+
})
|
|
446
|
+
.on("tool:result", (e) => {
|
|
447
|
+
metrics.histogram("tool.duration", e.durationMs);
|
|
448
|
+
if (e.isError) metrics.increment("tool.errors");
|
|
449
|
+
})
|
|
450
|
+
.on("gate:denied", (e) => {
|
|
451
|
+
audit.log(`${e.toolName} denied: ${e.reason}`);
|
|
452
|
+
})
|
|
453
|
+
.on("tools:unlocked", (e) => {
|
|
454
|
+
console.log(`${e.toolNames.join(", ")} unlocked via ${e.prerequisite}`);
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Events: `tool:call`, `tool:result`, `gate:denied`, `auth:success`, `auth:failure`, `tools:promoted`, `tools:unlocked`, `deploy:start`, `deploy:complete`.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Session Stores
|
|
463
|
+
|
|
464
|
+
### Memory (dev/test)
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { MemorySessionStore } from "@usestratus/mcp-aws";
|
|
468
|
+
|
|
469
|
+
server.lambda({ sessionStore: new MemorySessionStore({ ttlMs: 3600_000 }) });
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### DynamoDB (production)
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { DynamoSessionStore } from "@usestratus/mcp-aws/dynamo";
|
|
476
|
+
|
|
477
|
+
server.lambda({
|
|
478
|
+
sessionStore: new DynamoSessionStore({
|
|
479
|
+
tableName: "mcp-sessions",
|
|
480
|
+
region: "us-east-1",
|
|
481
|
+
ttlSeconds: 86400,
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### SQLite (ECS/Fargate/EC2)
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { SqliteSessionStore } from "@usestratus/mcp-aws";
|
|
490
|
+
|
|
491
|
+
server.lambda({
|
|
492
|
+
sessionStore: new SqliteSessionStore({
|
|
493
|
+
path: "/tmp/mcp-sessions.db",
|
|
494
|
+
ttlMs: 3_600_000,
|
|
495
|
+
}),
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Uses Bun's native `bun:sqlite`. Zero dependencies. Supports file persistence across restarts.
|
|
500
|
+
|
|
501
|
+
### Custom
|
|
502
|
+
|
|
503
|
+
Implement the `SessionStore` interface:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
import type { SessionStore, McpSession } from "@usestratus/mcp-aws";
|
|
507
|
+
|
|
508
|
+
class RedisSessionStore implements SessionStore {
|
|
509
|
+
async get(sessionId: string): Promise<McpSession | undefined> { /* ... */ }
|
|
510
|
+
async set(session: McpSession): Promise<void> { /* ... */ }
|
|
511
|
+
async delete(sessionId: string): Promise<void> { /* ... */ }
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## RFC 9728 OAuth Metadata
|
|
518
|
+
|
|
519
|
+
Automatically serve `.well-known/oauth-protected-resource` for MCP clients that support OAuth discovery:
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
server.lambda({
|
|
523
|
+
baseUrl: "https://api.example.com",
|
|
524
|
+
resourceMetadata: {
|
|
525
|
+
baseUrl: "https://api.example.com",
|
|
526
|
+
authorizationServers: ["https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx"],
|
|
527
|
+
scopes: ["openid", "email"],
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
401 responses include `WWW-Authenticate: Bearer realm="mcp-server", resource_metadata="..."` per the spec.
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Error Classes
|
|
537
|
+
|
|
538
|
+
| Error | Description |
|
|
539
|
+
|---|---|
|
|
540
|
+
| `McpAwsError` | Base error |
|
|
541
|
+
| `GateDeniedError` | Tool gate blocked (has `toolName`, `reason`, `hint`) |
|
|
542
|
+
| `AuthenticationError` | Auth failed |
|
|
543
|
+
| `SessionNotFoundError` | Session expired/missing (has `sessionId`) |
|
|
544
|
+
| `ToolExecutionError` | Tool handler threw (has `toolName`, `cause`) |
|
|
545
|
+
| `ToolTimeoutError` | Tool exceeded timeout (has `toolName`, `timeoutMs`) |
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Full Example: Playwright MCP on Lambda
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import { McpServer, apiKey, role } from "@usestratus/mcp-aws";
|
|
553
|
+
import { z } from "zod";
|
|
554
|
+
|
|
555
|
+
const server = new McpServer("playwright@1.0.0")
|
|
556
|
+
.auth(apiKey({ "demo-key": { roles: ["user"] } }))
|
|
557
|
+
|
|
558
|
+
// Always visible (3 core tools)
|
|
559
|
+
.tool("browser_navigate", z.object({ url: z.string() }), async ({ url }) => {
|
|
560
|
+
return { navigated: true, url };
|
|
561
|
+
})
|
|
562
|
+
.tool("browser_snapshot", async () => {
|
|
563
|
+
return { title: "Page", elements: ["heading", "link", "form"] };
|
|
564
|
+
})
|
|
565
|
+
.tool("browser_close", async () => "closed")
|
|
566
|
+
|
|
567
|
+
// Discoverable via search (interaction tools)
|
|
568
|
+
.tool("browser_click", {
|
|
569
|
+
tier: "discoverable",
|
|
570
|
+
tags: ["interaction"],
|
|
571
|
+
params: z.object({ element: z.string() }),
|
|
572
|
+
}, async ({ element }) => `Clicked ${element}`)
|
|
573
|
+
|
|
574
|
+
.tool("browser_fill", {
|
|
575
|
+
tier: "discoverable",
|
|
576
|
+
tags: ["form", "input"],
|
|
577
|
+
params: z.object({ element: z.string(), value: z.string() }),
|
|
578
|
+
}, async ({ element, value }) => `Filled "${element}" with "${value}"`)
|
|
579
|
+
|
|
580
|
+
// Hidden until auth (debug tools)
|
|
581
|
+
.tool("browser_console", {
|
|
582
|
+
tier: "hidden",
|
|
583
|
+
gate: role("user"),
|
|
584
|
+
tags: ["debug"],
|
|
585
|
+
}, async () => [{ level: "log", text: "loaded" }])
|
|
586
|
+
|
|
587
|
+
// Observability
|
|
588
|
+
.on("tool:call", (e) => console.log(`[${e.toolName}] called`));
|
|
589
|
+
|
|
590
|
+
export const handler = server.lambda();
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
19 tools → only 3 + `search_tools` visible initially. The agent discovers the rest via search. Debug tools require authentication. All deployed with `server.lambda()`.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Development
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
bun install # Install dependencies
|
|
601
|
+
bun test # Run 248 tests (<1s)
|
|
602
|
+
bun run typecheck # TypeScript checking
|
|
603
|
+
bun run lint # Biome linting
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Tests are organized by type:
|
|
607
|
+
- **Unit** (125 tests) — types, gates, search, auth, session, codemode, context, ssrf, events
|
|
608
|
+
- **Local integration** (80 tests) — Lambda handler, MCP protocol, disclosure, PRD stories
|
|
609
|
+
- **Edge cases** (43 tests) — every branch covered
|
|
610
|
+
- **AWS integration** (optional) — real Lambda deploy + Function URL + Stratus agent E2E
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AuthContext } from "../types.js";
|
|
2
|
+
import type { AuthProvider, AuthRequest } from "./types.js";
|
|
3
|
+
export type ApiKeyEntry = {
|
|
4
|
+
subject?: string;
|
|
5
|
+
roles?: string[];
|
|
6
|
+
claims?: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
export type ApiKeyAuthConfig = {
|
|
9
|
+
keys: Record<string, ApiKeyEntry>;
|
|
10
|
+
headerName?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Factory: create an API key auth provider.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* server.auth(apiKey({ "sk-123": { roles: ["admin"] } }));
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function apiKey(keys: Record<string, ApiKeyEntry>, opts?: {
|
|
21
|
+
headerName?: string;
|
|
22
|
+
}): AuthProvider;
|
|
23
|
+
export declare class ApiKeyAuth implements AuthProvider {
|
|
24
|
+
private readonly keys;
|
|
25
|
+
private readonly headerName;
|
|
26
|
+
constructor(config: ApiKeyAuthConfig);
|
|
27
|
+
authenticate(request: AuthRequest): Promise<AuthContext>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.d.ts","sourceRoot":"","sources":["../../src/auth/api-key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,MAAM,WAAW,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAQF;;;;;;;GAOG;AACH,wBAAgB,MAAM,CACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACjC,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,YAAY,CAEd;AAED,qBAAa,UAAW,YAAW,YAAY;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,MAAM,EAAE,gBAAgB;IAK9B,YAAY,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;CAoB9D"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const UNAUTHENTICATED = {
|
|
2
|
+
authenticated: false,
|
|
3
|
+
roles: [],
|
|
4
|
+
claims: {},
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Factory: create an API key auth provider.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* server.auth(apiKey({ "sk-123": { roles: ["admin"] } }));
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function apiKey(keys, opts) {
|
|
15
|
+
return new ApiKeyAuth({ keys, headerName: opts?.headerName });
|
|
16
|
+
}
|
|
17
|
+
export class ApiKeyAuth {
|
|
18
|
+
keys;
|
|
19
|
+
headerName;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.keys = config.keys;
|
|
22
|
+
this.headerName = config.headerName ?? "x-api-key";
|
|
23
|
+
}
|
|
24
|
+
async authenticate(request) {
|
|
25
|
+
const raw = request.headers[this.headerName];
|
|
26
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
27
|
+
if (!value) {
|
|
28
|
+
return UNAUTHENTICATED;
|
|
29
|
+
}
|
|
30
|
+
const entry = this.keys[value];
|
|
31
|
+
if (!entry) {
|
|
32
|
+
return UNAUTHENTICATED;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
authenticated: true,
|
|
36
|
+
subject: entry.subject,
|
|
37
|
+
roles: entry.roles ?? [],
|
|
38
|
+
claims: entry.claims ?? {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.js","sourceRoot":"","sources":["../../src/auth/api-key.ts"],"names":[],"mappings":"AAcA,MAAM,eAAe,GAAgB;IACpC,aAAa,EAAE,KAAK;IACpB,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;CACV,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CACrB,IAAiC,EACjC,IAA8B;IAE9B,OAAO,IAAI,UAAU,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,OAAO,UAAU;IACL,IAAI,CAA8B;IAClC,UAAU,CAAS;IAEpC,YAAY,MAAwB;QACnC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,WAAW,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAoB;QACtC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAEhD,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,eAAe,CAAC;QACxB,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,eAAe,CAAC;QACxB,CAAC;QAED,OAAO;YACN,aAAa,EAAE,IAAI;YACnB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE;YACxB,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE;SAC1B,CAAC;IACH,CAAC;CACD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AuthContext } from "../types.js";
|
|
2
|
+
import type { AuthProvider, AuthRequest } from "./types.js";
|
|
3
|
+
export type CognitoAuthConfig = {
|
|
4
|
+
userPoolId: string;
|
|
5
|
+
region: string;
|
|
6
|
+
audience?: string;
|
|
7
|
+
rolesClaim?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Factory: create a Cognito JWT auth provider.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* server.auth(cognito({ userPoolId: "us-east-1_abc", region: "us-east-1" }));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function cognito(config: CognitoAuthConfig): AuthProvider;
|
|
18
|
+
export declare class CognitoAuth implements AuthProvider {
|
|
19
|
+
private readonly issuer;
|
|
20
|
+
private readonly jwks;
|
|
21
|
+
private readonly audience?;
|
|
22
|
+
private readonly rolesClaim;
|
|
23
|
+
constructor(config: CognitoAuthConfig);
|
|
24
|
+
authenticate(request: AuthRequest): Promise<AuthContext>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=cognito.d.ts.map
|