fa-mcp-sdk 0.4.142 → 0.11.2
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 +5 -0
- package/cli-template/.dockerignore +16 -0
- package/cli-template/.gitlab-ci.yml +135 -0
- package/cli-template/AGENTS.md +1 -0
- package/cli-template/CHANGELOG.md +64 -0
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +27 -4
- package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +195 -0
- package/cli-template/FA-MCP-SDK-DOC/02-2-prompts-and-resources.md +172 -9
- package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +170 -12
- package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +158 -8
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +67 -6
- package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +31 -15
- package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/11-public-contract.md +342 -0
- package/cli-template/README.md +37 -0
- package/cli-template/deploy/docker/.env.example +10 -0
- package/cli-template/deploy/docker/Dockerfile +44 -0
- package/cli-template/deploy/docker/Dockerfile.local +29 -0
- package/cli-template/deploy/docker/README.md +94 -0
- package/cli-template/deploy/docker/config/local.docker.yaml +14 -0
- package/cli-template/deploy/docker/docker-compose.yml +31 -0
- package/cli-template/deploy/gitlab-runner/.env.example +16 -0
- package/cli-template/deploy/gitlab-runner/README.md +65 -0
- package/cli-template/deploy/gitlab-runner/config/config.toml.template +26 -0
- package/cli-template/deploy/gitlab-runner/docker-compose.yml +39 -0
- package/cli-template/deploy/gitlab-runner/entrypoint.sh +27 -0
- package/cli-template/deploy/gitlab-runner/start.sh +47 -0
- package/cli-template/gitignore +96 -95
- package/cli-template/package.json +1 -1
- package/config/_local.yaml +73 -11
- package/config/custom-environment-variables.yaml +102 -0
- package/config/default.yaml +164 -11
- package/config/local.yaml +20 -19
- package/dist/core/_types_/config.d.ts +119 -0
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/_types_/types.d.ts +137 -4
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.js +25 -11
- package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.js +6 -4
- package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
- package/dist/core/auth/admin-auth.js +4 -4
- package/dist/core/auth/admin-auth.js.map +1 -1
- package/dist/core/auth/agent-tester-auth.d.ts +1 -1
- package/dist/core/auth/agent-tester-auth.d.ts.map +1 -1
- package/dist/core/auth/agent-tester-auth.js +8 -4
- package/dist/core/auth/agent-tester-auth.js.map +1 -1
- package/dist/core/auth/auth-profile.d.ts +38 -0
- package/dist/core/auth/auth-profile.d.ts.map +1 -0
- package/dist/core/auth/auth-profile.js +101 -0
- package/dist/core/auth/auth-profile.js.map +1 -0
- package/dist/core/auth/jwt-v2.d.ts +27 -0
- package/dist/core/auth/jwt-v2.d.ts.map +1 -0
- package/dist/core/auth/jwt-v2.js +180 -0
- package/dist/core/auth/jwt-v2.js.map +1 -0
- package/dist/core/auth/jwt.d.ts +27 -13
- package/dist/core/auth/jwt.d.ts.map +1 -1
- package/dist/core/auth/jwt.js +36 -13
- package/dist/core/auth/jwt.js.map +1 -1
- package/dist/core/auth/key-resolver.d.ts +74 -0
- package/dist/core/auth/key-resolver.d.ts.map +1 -0
- package/dist/core/auth/key-resolver.js +330 -0
- package/dist/core/auth/key-resolver.js.map +1 -0
- package/dist/core/auth/middleware.d.ts.map +1 -1
- package/dist/core/auth/middleware.js +66 -0
- package/dist/core/auth/middleware.js.map +1 -1
- package/dist/core/auth/multi-auth.d.ts +1 -1
- package/dist/core/auth/multi-auth.d.ts.map +1 -1
- package/dist/core/auth/multi-auth.js +7 -7
- package/dist/core/auth/multi-auth.js.map +1 -1
- package/dist/core/auth/token-generator/server.js +4 -4
- package/dist/core/auth/token-generator/server.js.map +1 -1
- package/dist/core/auth/types.d.ts +5 -0
- package/dist/core/auth/types.d.ts.map +1 -1
- package/dist/core/db/pg-db.d.ts +7 -0
- package/dist/core/db/pg-db.d.ts.map +1 -1
- package/dist/core/db/pg-db.js +54 -3
- package/dist/core/db/pg-db.js.map +1 -1
- package/dist/core/errors/BaseMcpError.d.ts +21 -1
- package/dist/core/errors/BaseMcpError.d.ts.map +1 -1
- package/dist/core/errors/BaseMcpError.js +20 -1
- package/dist/core/errors/BaseMcpError.js.map +1 -1
- package/dist/core/errors/ValidationError.d.ts +5 -0
- package/dist/core/errors/ValidationError.d.ts.map +1 -1
- package/dist/core/errors/ValidationError.js +6 -1
- package/dist/core/errors/ValidationError.js.map +1 -1
- package/dist/core/errors/errors.d.ts +31 -3
- package/dist/core/errors/errors.d.ts.map +1 -1
- package/dist/core/errors/errors.js +86 -6
- package/dist/core/errors/errors.js.map +1 -1
- package/dist/core/errors/specific-errors.d.ts +54 -0
- package/dist/core/errors/specific-errors.d.ts.map +1 -0
- package/dist/core/errors/specific-errors.js +82 -0
- package/dist/core/errors/specific-errors.js.map +1 -0
- package/dist/core/index.d.ts +10 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/init-mcp-server.d.ts.map +1 -1
- package/dist/core/init-mcp-server.js +39 -0
- package/dist/core/init-mcp-server.js.map +1 -1
- package/dist/core/mcp/create-mcp-server.d.ts +12 -6
- package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
- package/dist/core/mcp/create-mcp-server.js +592 -33
- package/dist/core/mcp/create-mcp-server.js.map +1 -1
- package/dist/core/mcp/debug-trace.d.ts +3 -1
- package/dist/core/mcp/debug-trace.d.ts.map +1 -1
- package/dist/core/mcp/debug-trace.js +17 -2
- package/dist/core/mcp/debug-trace.js.map +1 -1
- package/dist/core/mcp/deprecation.d.ts +31 -0
- package/dist/core/mcp/deprecation.d.ts.map +1 -0
- package/dist/core/mcp/deprecation.js +96 -0
- package/dist/core/mcp/deprecation.js.map +1 -0
- package/dist/core/mcp/mcp-logging.d.ts +32 -0
- package/dist/core/mcp/mcp-logging.d.ts.map +1 -0
- package/dist/core/mcp/mcp-logging.js +97 -0
- package/dist/core/mcp/mcp-logging.js.map +1 -0
- package/dist/core/mcp/pagination.d.ts +13 -0
- package/dist/core/mcp/pagination.d.ts.map +1 -0
- package/dist/core/mcp/pagination.js +50 -0
- package/dist/core/mcp/pagination.js.map +1 -0
- package/dist/core/mcp/prompts.d.ts +5 -1
- package/dist/core/mcp/prompts.d.ts.map +1 -1
- package/dist/core/mcp/prompts.js +3 -1
- package/dist/core/mcp/prompts.js.map +1 -1
- package/dist/core/mcp/resources.d.ts +9 -0
- package/dist/core/mcp/resources.d.ts.map +1 -1
- package/dist/core/mcp/resources.js +158 -11
- package/dist/core/mcp/resources.js.map +1 -1
- package/dist/core/mcp/server-stdio.d.ts +7 -1
- package/dist/core/mcp/server-stdio.d.ts.map +1 -1
- package/dist/core/mcp/server-stdio.js +8 -3
- package/dist/core/mcp/server-stdio.js.map +1 -1
- package/dist/core/mcp/task-store.d.ts +97 -0
- package/dist/core/mcp/task-store.d.ts.map +1 -0
- package/dist/core/mcp/task-store.js +175 -0
- package/dist/core/mcp/task-store.js.map +1 -0
- package/dist/core/mcp/tool-limits.d.ts +22 -0
- package/dist/core/mcp/tool-limits.d.ts.map +1 -0
- package/dist/core/mcp/tool-limits.js +115 -0
- package/dist/core/mcp/tool-limits.js.map +1 -0
- package/dist/core/mcp/validate-tool-args.d.ts +16 -0
- package/dist/core/mcp/validate-tool-args.d.ts.map +1 -0
- package/dist/core/mcp/validate-tool-args.js +67 -0
- package/dist/core/mcp/validate-tool-args.js.map +1 -0
- package/dist/core/mcp/validate-tool-names.d.ts +11 -0
- package/dist/core/mcp/validate-tool-names.d.ts.map +1 -0
- package/dist/core/mcp/validate-tool-names.js +23 -0
- package/dist/core/mcp/validate-tool-names.js.map +1 -0
- package/dist/core/metrics/metrics.d.ts +45 -0
- package/dist/core/metrics/metrics.d.ts.map +1 -0
- package/dist/core/metrics/metrics.js +119 -0
- package/dist/core/metrics/metrics.js.map +1 -0
- package/dist/core/utils/mask-sensitive.d.ts +44 -0
- package/dist/core/utils/mask-sensitive.d.ts.map +1 -0
- package/dist/core/utils/mask-sensitive.js +64 -0
- package/dist/core/utils/mask-sensitive.js.map +1 -0
- package/dist/core/utils/testing/McpHttpClient.d.ts +8 -33
- package/dist/core/utils/testing/McpHttpClient.d.ts.map +1 -1
- package/dist/core/utils/testing/McpHttpClient.js +8 -74
- package/dist/core/utils/testing/McpHttpClient.js.map +1 -1
- package/dist/core/utils/testing/McpStreamableHttpClient.d.ts +24 -30
- package/dist/core/utils/testing/McpStreamableHttpClient.d.ts.map +1 -1
- package/dist/core/utils/testing/McpStreamableHttpClient.js +36 -198
- package/dist/core/utils/testing/McpStreamableHttpClient.js.map +1 -1
- package/dist/core/utils/utils.d.ts.map +1 -1
- package/dist/core/utils/utils.js +2 -0
- package/dist/core/utils/utils.js.map +1 -1
- package/dist/core/web/admin-router.js +3 -3
- package/dist/core/web/admin-router.js.map +1 -1
- package/dist/core/web/cors.d.ts +9 -1
- package/dist/core/web/cors.d.ts.map +1 -1
- package/dist/core/web/cors.js +26 -5
- package/dist/core/web/cors.js.map +1 -1
- package/dist/core/web/event-store.d.ts +33 -0
- package/dist/core/web/event-store.d.ts.map +1 -0
- package/dist/core/web/event-store.js +65 -0
- package/dist/core/web/event-store.js.map +1 -0
- package/dist/core/web/oauth-router.d.ts +37 -0
- package/dist/core/web/oauth-router.d.ts.map +1 -0
- package/dist/core/web/oauth-router.js +207 -0
- package/dist/core/web/oauth-router.js.map +1 -0
- package/dist/core/web/request-id.d.ts +44 -0
- package/dist/core/web/request-id.d.ts.map +1 -0
- package/dist/core/web/request-id.js +82 -0
- package/dist/core/web/request-id.js.map +1 -0
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +322 -182
- package/dist/core/web/server-http.js.map +1 -1
- package/package.json +15 -2
- package/scripts/claude-2-agents-symlink.js +10 -1
- package/scripts/generate-jwt.js +129 -51
- package/src/template/custom-resources.ts +14 -0
- package/src/template/prompts/custom-prompts.ts +4 -0
- package/src/template/tools/handle-tool-call.ts +59 -3
- package/src/template/tools/tools.ts +92 -31
- package/src/tests/mcp/test-http.js +1 -1
- package/src/tests/mcp/test-sse.js +1 -1
|
@@ -28,20 +28,47 @@ export const AGENT_PROMPT = `You are a database management assistant.
|
|
|
28
28
|
Add in `src/prompts/custom-prompts.ts`:
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import { IPromptData, IGetPromptRequest } from 'fa-mcp-sdk';
|
|
31
|
+
import { IPromptData, IGetPromptRequest, IPromptArgument } from 'fa-mcp-sdk';
|
|
32
32
|
|
|
33
33
|
export const customPrompts: IPromptData[] = [
|
|
34
34
|
{ name: 'greeting', description: 'Greeting message', arguments: [],
|
|
35
35
|
content: 'Hello! How can I help?' },
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Standard §10.5 — parameterised prompt. The `arguments[]` array is advertised in
|
|
38
|
+
// prompts/list; the values arrive as `request.params.arguments` (string map) on
|
|
39
|
+
// prompts/get. The content function receives them as the second argument.
|
|
40
|
+
{
|
|
41
|
+
name: 'context_prompt',
|
|
42
|
+
description: 'Context-aware prompt with explicit arguments',
|
|
43
|
+
arguments: [
|
|
44
|
+
{ name: 'topic', description: 'Subject area to focus on', required: true },
|
|
45
|
+
{ name: 'audience', description: 'Audience level (junior / senior)', required: false },
|
|
46
|
+
] satisfies IPromptArgument[],
|
|
47
|
+
content: (_req, args) =>
|
|
48
|
+
`Focus on ${args?.topic ?? 'the codebase'} for a ${args?.audience ?? 'mixed'} audience.`,
|
|
49
|
+
},
|
|
39
50
|
|
|
40
51
|
{ name: 'admin_only', description: 'Admin instructions', arguments: [],
|
|
41
52
|
content: 'Admin-only content', requireAuth: true },
|
|
53
|
+
|
|
54
|
+
// Standard §10.5 (MAY) — optional UI metadata. `title` is a human-facing label (falls back to
|
|
55
|
+
// `name`); `icons` is an `IIcon[]` (`{ src; mimeType?; sizes? }`, `src` = absolute URL or data: URI).
|
|
56
|
+
// Both only affect display in the client UI and pass through prompts/list unchanged.
|
|
57
|
+
{
|
|
58
|
+
name: 'release_notes',
|
|
59
|
+
title: 'Release notes',
|
|
60
|
+
icons: [{ src: 'https://cdn.example.com/notes.png', mimeType: 'image/png', sizes: '48x48' }],
|
|
61
|
+
description: 'Release change summary',
|
|
62
|
+
arguments: [],
|
|
63
|
+
content: 'Summary of changes for the current release.',
|
|
64
|
+
},
|
|
42
65
|
];
|
|
43
66
|
```
|
|
44
67
|
|
|
68
|
+
> **Compatibility.** The old single-argument signature
|
|
69
|
+
> `(req: IGetPromptRequest) => string` still works — the second `args` parameter is
|
|
70
|
+
> optional. Only update prompts that need access to the values.
|
|
71
|
+
|
|
45
72
|
Pass to server:
|
|
46
73
|
```typescript
|
|
47
74
|
const serverData: McpServerData = { ..., customPrompts };
|
|
@@ -114,12 +141,21 @@ export const customPrompts = async (ctx: ITransportContext): Promise<IPromptData
|
|
|
114
141
|
|
|
115
142
|
### Standard Resources
|
|
116
143
|
|
|
117
|
-
| URI | Description |
|
|
118
|
-
|
|
119
|
-
| `project://id` | Service identifier (`appConfig.name`) |
|
|
120
|
-
| `project://name` | Display name (`appConfig.productName`) |
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
144
|
+
| URI | MIME | Description |
|
|
145
|
+
|-----|------|-------------|
|
|
146
|
+
| `project://id` | `text/plain` | Service identifier (`appConfig.name`) |
|
|
147
|
+
| `project://name` | `text/plain` | Display name (`appConfig.productName`) |
|
|
148
|
+
| `project://version` | `text/plain` | Server version (`appConfig.version`) — mirror of `GET /health.version` and `serverInfo.version` (standard §4 SHOULD) |
|
|
149
|
+
| `doc://readme` | `text/markdown` | README.md content |
|
|
150
|
+
| `use://http-headers` | `application/json` | Used HTTP headers (from `usedHttpHeaders`) |
|
|
151
|
+
| `use://auth` | `application/json` | Enabled auth schemes / methods / expected JWT claims (standard §11.2 SHOULD) |
|
|
152
|
+
| `<appConfig.name>://agent/brief` | `text/markdown` | Mirror of `agent_brief` prompt (Avatar profile §11.2) |
|
|
153
|
+
| `<appConfig.name>://agent/prompt` | `text/markdown` | Mirror of `agent_prompt` prompt (Avatar profile §11.2) |
|
|
154
|
+
|
|
155
|
+
> The `<appConfig.name>://agent/*` URIs are built automatically from `appConfig.name`
|
|
156
|
+
> (e.g. `mcp-jira://agent/brief`). If a project's `customResources` list contains a
|
|
157
|
+
> resource with the same URI, the project-supplied entry wins — handy when the service
|
|
158
|
+
> needs to publish a different brief through the resources endpoint than through prompts.
|
|
123
159
|
|
|
124
160
|
### Custom Resources
|
|
125
161
|
|
|
@@ -142,6 +178,15 @@ export const customResources: IResourceData[] = [
|
|
|
142
178
|
|
|
143
179
|
{ uri: 'custom://secrets', name: 'Secrets', description: 'Protected',
|
|
144
180
|
mimeType: 'application/json', content: {}, requireAuth: true },
|
|
181
|
+
|
|
182
|
+
// Standard §11.3 (MAY) — optional UI metadata. `title` is a human-facing label; `icons` is an
|
|
183
|
+
// `IIcon[]` (same shape as prompts). `size` (bytes) is optional: on resources/list the SDK
|
|
184
|
+
// computes it from the content (UTF-8 byte length for text/objects, buffer length for blobs) when
|
|
185
|
+
// not set; lazy (function) content omits `size`. An author-supplied `size` is preserved.
|
|
186
|
+
{ uri: 'custom://logo', name: 'logo', title: 'Brand logo', description: 'SVG logo',
|
|
187
|
+
mimeType: 'image/svg+xml', size: 1234,
|
|
188
|
+
icons: [{ src: 'https://cdn.example.com/logo.svg', mimeType: 'image/svg+xml' }],
|
|
189
|
+
content: '<svg …>' },
|
|
145
190
|
];
|
|
146
191
|
```
|
|
147
192
|
|
|
@@ -150,6 +195,35 @@ Pass to server:
|
|
|
150
195
|
const serverData: McpServerData = { ..., customResources };
|
|
151
196
|
```
|
|
152
197
|
|
|
198
|
+
### Binary Resources (`blob`)
|
|
199
|
+
|
|
200
|
+
A resource whose payload is not text (image, PDF, archive, …) declares `content` as
|
|
201
|
+
`IResourceBinaryContent` instead of a string. `resources/read` then returns the bytes as base64
|
|
202
|
+
`contents[0].blob` (with the resource's `mimeType`) and omits `text` — exactly one of `text` /
|
|
203
|
+
`blob` is present per standard §11.4 / §12.2.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { readFileSync } from 'node:fs';
|
|
207
|
+
import { IResourceData } from 'fa-mcp-sdk';
|
|
208
|
+
|
|
209
|
+
export const customResources: IResourceData[] = [
|
|
210
|
+
// Raw bytes — the SDK base64-encodes the Buffer for you:
|
|
211
|
+
{ uri: 'custom://logo.png', name: 'Logo', description: 'Brand logo',
|
|
212
|
+
mimeType: 'image/png', content: { blob: readFileSync('assets/logo.png') } },
|
|
213
|
+
|
|
214
|
+
// Already-base64 string — pass it through with base64: true:
|
|
215
|
+
{ uri: 'custom://icon.png', name: 'Icon', description: 'App icon',
|
|
216
|
+
mimeType: 'image/png', content: { blob: PNG_BASE64, base64: true } },
|
|
217
|
+
|
|
218
|
+
// A function may return binary content too (sync or async):
|
|
219
|
+
{ uri: 'custom://report.pdf', name: 'Report', description: 'Generated PDF',
|
|
220
|
+
mimeType: 'application/pdf', content: async () => ({ blob: await buildPdf() }) },
|
|
221
|
+
];
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`{ blob: string }` is assumed to be base64 unless you set `base64: false` (then the SDK encodes the
|
|
225
|
+
string's raw bytes). Clients decode `contents[0].blob` from base64 to recover the original file.
|
|
226
|
+
|
|
153
227
|
### Dynamic Resources (Function)
|
|
154
228
|
|
|
155
229
|
For dynamic resource lists based on transport type, headers, or user:
|
|
@@ -203,3 +277,92 @@ Both prompts and resources support `requireAuth: true`:
|
|
|
203
277
|
- Requires valid authentication to access
|
|
204
278
|
- Unauthenticated requests get error
|
|
205
279
|
- Works with any configured auth method (JWT, Basic, etc.)
|
|
280
|
+
|
|
281
|
+
## Optional MAY capabilities — templates & subscribe (standard §11.5)
|
|
282
|
+
|
|
283
|
+
Disabled by default. Opt-in via `config/default.yaml`:
|
|
284
|
+
|
|
285
|
+
```yaml
|
|
286
|
+
mcp:
|
|
287
|
+
resources:
|
|
288
|
+
subscribeEnabled: false # MAY §11.5 — turn on only when resources change at runtime
|
|
289
|
+
templatesEnabled: false # MAY §11.5 — turn on when you publish customResourceTemplates
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `resources/templates/list`
|
|
293
|
+
|
|
294
|
+
When `templatesEnabled: true`, register `customResourceTemplates` on `McpServerData`:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { IResourceTemplateInfo, McpServerData } from 'fa-mcp-sdk';
|
|
298
|
+
|
|
299
|
+
const customResourceTemplates: IResourceTemplateInfo[] = [
|
|
300
|
+
{
|
|
301
|
+
uriTemplate: 'issue://{key}', // RFC 6570
|
|
302
|
+
name: 'jira-issue',
|
|
303
|
+
title: 'Jira issue by key',
|
|
304
|
+
description: 'Single Jira issue addressable by ticket key.',
|
|
305
|
+
mimeType: 'application/json',
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const serverData: McpServerData = { ..., customResourceTemplates };
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
If you do not register any templates the server still answers `resources/templates/list`
|
|
313
|
+
with an empty array — clients can probe the capability safely.
|
|
314
|
+
|
|
315
|
+
### `resources/subscribe` + change notifications
|
|
316
|
+
|
|
317
|
+
When `subscribeEnabled: true`, the server advertises `subscribe` and `listChanged` in its
|
|
318
|
+
`resources` capability. To notify subscribers when content changes call
|
|
319
|
+
`notifyResourceUpdated(server, uri)`:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { notifyResourceUpdated } from 'fa-mcp-sdk';
|
|
323
|
+
|
|
324
|
+
// Each HTTP session owns its own Server instance — track the server reference at the
|
|
325
|
+
// point where you have it (e.g. inside a custom-resources content function).
|
|
326
|
+
await notifyResourceUpdated(server, 'project://version');
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The helper emits `notifications/resources/updated` only to clients that previously called
|
|
330
|
+
`resources/subscribe` for the given URI on that `Server`.
|
|
331
|
+
|
|
332
|
+
## Optional MAY capability — argument completion (standard §8.2)
|
|
333
|
+
|
|
334
|
+
`completion/complete` lets a client ask the server to suggest values for a prompt or resource
|
|
335
|
+
argument (for example, the valid project ids for a `project` argument). Disabled by default; the
|
|
336
|
+
capability is advertised only when **both** the config flag is on **and** a `completionProvider`
|
|
337
|
+
is supplied on `McpServerData` — otherwise `completion/complete` returns `-32601`.
|
|
338
|
+
|
|
339
|
+
```yaml
|
|
340
|
+
mcp:
|
|
341
|
+
completions:
|
|
342
|
+
enabled: true # MAY §8.2 — also requires a completionProvider (see below)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { McpServerData } from 'fa-mcp-sdk';
|
|
347
|
+
|
|
348
|
+
const completionProvider: McpServerData['completionProvider'] = async ({ ref, argument }) => {
|
|
349
|
+
// ref: { type: 'ref/prompt' | 'ref/resource'; name?; uri? }
|
|
350
|
+
// argument: { name; value } — value is what the user has typed so far
|
|
351
|
+
if (ref.type === 'ref/prompt' && argument.name === 'project') {
|
|
352
|
+
const all = await listProjectIds();
|
|
353
|
+
return all.filter((id) => id.startsWith(argument.value));
|
|
354
|
+
}
|
|
355
|
+
return [];
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const serverData: McpServerData = { ..., completionProvider };
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The SDK caps the response at 100 values and sets `completion.hasMore` / `completion.total`
|
|
362
|
+
accordingly.
|
|
363
|
+
|
|
364
|
+
## Pagination (standard §8.4)
|
|
365
|
+
|
|
366
|
+
`prompts/list` and `resources/list` use the same cursor-based pagination as `tools/list`:
|
|
367
|
+
opaque base64(offset), stable sort by `name` / `uri`. The page size comes from
|
|
368
|
+
`mcp.pagination.pageSize` (default 100). See [03-configuration → "Pagination"](./03-configuration.md#pagination).
|
|
@@ -113,9 +113,46 @@ mcp:
|
|
|
113
113
|
rateLimit:
|
|
114
114
|
maxRequests: 100
|
|
115
115
|
windowMs: 60000
|
|
116
|
+
# Standard §14 — 'subject' counts per JWT `sub`/`user` with IP fallback; 'ip' = legacy.
|
|
117
|
+
scope: subject
|
|
118
|
+
# Max in-flight tools/call per subject. Excess → -32003 / HTTP 429 + Retry-After.
|
|
119
|
+
maxConcurrentPerSubject: 16
|
|
120
|
+
# Hard ceilings enforced by the HTTP transport (standard §14). Concrete servers MAY raise
|
|
121
|
+
# or lower these per-environment without patching the SDK.
|
|
122
|
+
limits:
|
|
123
|
+
# Max accepted JSON / urlencoded request body, bytes. Above the limit:
|
|
124
|
+
# JSON-RPC code -32005, HTTP 413 Payload Too Large.
|
|
125
|
+
maxPayloadBytes: 1048576 # 1 MiB
|
|
126
|
+
# Max serialized tool result, bytes. Above the limit, the SDK truncates the payload
|
|
127
|
+
# and marks `structuredContent.truncated: true` + appends "…[truncated]" to text content.
|
|
128
|
+
maxToolResultBytes: 10485760 # 10 MiB
|
|
129
|
+
# Per-tool execution timeout, milliseconds. Above the limit:
|
|
130
|
+
# JSON-RPC code -32004, HTTP 504 Gateway Timeout.
|
|
131
|
+
toolTimeoutMs: 30000 # 30 seconds
|
|
116
132
|
tools:
|
|
117
133
|
answerAs: text # text | structuredContent
|
|
118
134
|
hideAnnotations: false # true — strip `annotations` from tool listings
|
|
135
|
+
# Standard §8.4 — server-side pagination for tools/list, prompts/list, resources/list.
|
|
136
|
+
pagination:
|
|
137
|
+
pageSize: 100 # items per page (cursor is opaque base64(offset))
|
|
138
|
+
# Standard §11.5 — optional MAY resource capabilities. Off by default.
|
|
139
|
+
resources:
|
|
140
|
+
subscribeEnabled: false # advertise `subscribe` + `listChanged`; emit notifications/resources/updated
|
|
141
|
+
templatesEnabled: false # advertise + serve resources/templates/list
|
|
142
|
+
# Standard §8.7 (MAY) — task-augmented execution (long-running / pollable tool calls). Off by
|
|
143
|
+
# default. When enabled, advertises the `tasks` capability and serves tasks/list|get|result|cancel.
|
|
144
|
+
# Long-running tools opt in per-tool via `execution.taskSupport`. Default store is in-memory only.
|
|
145
|
+
tasks:
|
|
146
|
+
enabled: false # advertise `tasks` capability and accept the lifecycle methods
|
|
147
|
+
defaultTtlMs: 3600000 # finished-task retention from creation (clamped to [minTtlMs, maxTtlMs])
|
|
148
|
+
minTtlMs: 0 # lower bound a client-requested ttl is clamped to
|
|
149
|
+
maxTtlMs: 86400000 # hard retention ceiling (24 h)
|
|
150
|
+
pollIntervalMs: 1000 # suggested client poll interval, surfaced in every task object
|
|
151
|
+
maxTasks: 1000 # retained tasks cap; oldest finished evicted first
|
|
152
|
+
# Standard §6 (MAY) — Streamable HTTP SSE resumability via Last-Event-ID. Off by default.
|
|
153
|
+
sse:
|
|
154
|
+
resumability: false # wire in-memory EventStore into the transport for replay on reconnect
|
|
155
|
+
maxStoredEvents: 1000 # ring-buffer size: recent events retained per process for replay
|
|
119
156
|
|
|
120
157
|
swagger:
|
|
121
158
|
servers:
|
|
@@ -135,10 +172,21 @@ uiColor:
|
|
|
135
172
|
primary: '#0f65dc'
|
|
136
173
|
|
|
137
174
|
webServer:
|
|
138
|
-
|
|
175
|
+
# Bind address. Default: '127.0.0.1' — loopback only (safer default, standard §6).
|
|
176
|
+
# Set to '0.0.0.0' explicitly when running inside a container / behind a reverse proxy.
|
|
177
|
+
host: '127.0.0.1'
|
|
139
178
|
port: {{port}}
|
|
140
|
-
#
|
|
141
|
-
|
|
179
|
+
# Array of hosts whose `Origin` header bypasses the CORS guard.
|
|
180
|
+
# CORS now actively rejects unlisted origins with HTTP 403 + JSON-RPC error.
|
|
181
|
+
# In production an empty list aborts startup.
|
|
182
|
+
originHosts: ['localhost']
|
|
183
|
+
# Express `trust proxy`. Set true | 'loopback' | <number> when behind an HTTPS reverse
|
|
184
|
+
# proxy so /.well-known/openid-configuration derives `issuer` from X-Forwarded-* headers.
|
|
185
|
+
trustProxy: false
|
|
186
|
+
# Standard §7.1 — secrets in URL forbidden. POST /ct with JSON body is the only safe form;
|
|
187
|
+
# GET /ct?t=<token> is disabled by default. Opt-in via allowQueryToken=true (non-prod only).
|
|
188
|
+
tokenCheck:
|
|
189
|
+
allowQueryToken: false
|
|
142
190
|
# Authentication is configured here only when accessing the MCP server
|
|
143
191
|
# Authentication in services that enable tools, resources, and prompts
|
|
144
192
|
# is implemented more deeply. To do this, you need to use the information passed in HTTP headers
|
|
@@ -156,17 +204,15 @@ webServer:
|
|
|
156
204
|
permanentServerTokens: [ ] # Add your server tokens here: ['token1', 'token2']
|
|
157
205
|
|
|
158
206
|
# ========================================================================
|
|
159
|
-
# JWT TOKEN —
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
# To enable this authentication, you need to set auth.enabled = true and set
|
|
166
|
-
# encryptKey to at least 8 characters (used as the HS256 signing secret).
|
|
207
|
+
# JWT TOKEN — four operating modes (since SDK 0.7.0)
|
|
208
|
+
# - legacyAesCtr (default) — HS256 + AES-CTR fallback. 0.6.x parity.
|
|
209
|
+
# - embedded — ES256/RS256 with built-in IdP. Dev / demo.
|
|
210
|
+
# - localKey — ES256/RS256 verify with public key on disk.
|
|
211
|
+
# - remoteJwks — verify against external IdP's JWKS endpoint.
|
|
167
212
|
# ========================================================================
|
|
168
213
|
jwtToken:
|
|
169
|
-
#
|
|
214
|
+
mode: legacyAesCtr # see above
|
|
215
|
+
# HS256 signing secret used ONLY by legacyAesCtr mode (minimum 8 chars)
|
|
170
216
|
encryptKey: '***'
|
|
171
217
|
# If webServer.auth.enabled and the parameter true, the service name and the service specified in the token will be checked
|
|
172
218
|
checkMCPName: true
|
|
@@ -174,8 +220,22 @@ webServer:
|
|
|
174
220
|
# the client IP will be checked against the allowed list in the token
|
|
175
221
|
isCheckIP: false
|
|
176
222
|
# Optional JWT `iss` claim. When non-empty, the generator stamps it and the verifier requires it.
|
|
223
|
+
# legacyAesCtr only — in non-legacy modes use expectedIssuer below.
|
|
177
224
|
issuer: ''
|
|
178
225
|
|
|
226
|
+
# -------- Modes embedded / localKey / remoteJwks --------
|
|
227
|
+
algorithm: ES256 # ES256 | RS256
|
|
228
|
+
keyStoragePath: './keys' # embedded: autogenerated keypair (private.pem + public.pem)
|
|
229
|
+
publicKeyPath: '' # localKey: PEM public key path
|
|
230
|
+
privateKeyPath: '' # localKey: optional — enables local issuance
|
|
231
|
+
jwksUri: '' # remoteJwks: external JWKS endpoint
|
|
232
|
+
expectedIssuer: '' # required for embedded/localKey/remoteJwks (standard §7.2)
|
|
233
|
+
expectedAudience: '' # defaults to appConfig.name
|
|
234
|
+
jwksCacheTtl: 600 # JWKS cache, seconds
|
|
235
|
+
jwksCooldown: 30 # min interval between repeat fetches when kid missing
|
|
236
|
+
clockSkew: 30 # allowed exp/nbf drift, seconds (max enforced: 60)
|
|
237
|
+
defaultTtl: 1800 # default TTL for /oauth/token-issued tokens
|
|
238
|
+
|
|
179
239
|
# ========================================================================
|
|
180
240
|
# Basic Authentication - Base64 encoded username:password
|
|
181
241
|
# CPU cost: Medium - Base64 decoding + string comparison
|
|
@@ -387,3 +447,101 @@ db:
|
|
|
387
447
|
password: <password>
|
|
388
448
|
usedExtensions: [] # e.g. [pgvector]
|
|
389
449
|
```
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
## Pagination (`mcp.pagination`)
|
|
453
|
+
|
|
454
|
+
Standard §8.4 — server-side pagination for `tools/list`, `prompts/list`, and
|
|
455
|
+
`resources/list`. The SDK sorts items stably by `name` / `uri`, slices the list, and
|
|
456
|
+
returns `nextCursor` (opaque base64 of the next offset) when more entries follow.
|
|
457
|
+
|
|
458
|
+
| Key | Default | Notes |
|
|
459
|
+
|-----|---------|-------|
|
|
460
|
+
| `mcp.pagination.pageSize` | `100` | Items per page; lower this for low-context clients (e.g. terminal MCP clients) or raise it for power users. |
|
|
461
|
+
|
|
462
|
+
Override per environment via `MCP_PAGINATION_PAGE_SIZE`. Invalid cursors return JSON-RPC
|
|
463
|
+
`-32602` with `error.data.field: 'cursor'`.
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
# clients without pagination support still see the first page — fully spec-compliant
|
|
467
|
+
curl -s ... -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
468
|
+
|
|
469
|
+
# clients that opt in
|
|
470
|
+
curl -s ... -d '{"jsonrpc":"2.0","method":"tools/list","id":1,"params":{"cursor":"NTA="}}'
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Resource MAY capabilities (`mcp.resources`)
|
|
474
|
+
|
|
475
|
+
Standard §11.5 — opt-in templates and subscriptions. Defaults keep the server in the
|
|
476
|
+
"static resources only" mode that low-context clients expect.
|
|
477
|
+
|
|
478
|
+
| Key | Default | Notes |
|
|
479
|
+
|-----|---------|-------|
|
|
480
|
+
| `mcp.resources.subscribeEnabled` | `false` | Advertise `subscribe` + `listChanged` and register `resources/subscribe`. Project code emits change events via `notifyResourceUpdated(server, uri)`. |
|
|
481
|
+
| `mcp.resources.templatesEnabled` | `false` | Advertise + serve `resources/templates/list`. Templates come from `McpServerData.customResourceTemplates`. |
|
|
482
|
+
|
|
483
|
+
See [02-2-prompts-and-resources → "Optional MAY capabilities"](./02-2-prompts-and-resources.md#optional-may-capabilities-templates--subscribe-standard-115)
|
|
484
|
+
for end-to-end examples.
|
|
485
|
+
|
|
486
|
+
## SSE stream resumability (`mcp.sse`)
|
|
487
|
+
|
|
488
|
+
Standard §6 (MAY) — opt-in replay of missed Streamable HTTP SSE events after a reconnect. Off by
|
|
489
|
+
default; when off the transport behaves exactly as before. Only relevant for the HTTP transport.
|
|
490
|
+
|
|
491
|
+
| Key | Default | Notes |
|
|
492
|
+
|-----|---------|-------|
|
|
493
|
+
| `mcp.sse.resumability` | `false` | When `true`, an in-memory `InMemoryEventStore` is wired into the Streamable HTTP transport. A client reconnecting to `GET /mcp` with a `Last-Event-ID` header replays the events it missed. Env `MCP_SSE_RESUMABILITY`. |
|
|
494
|
+
| `mcp.sse.maxStoredEvents` | `1000` | Ring-buffer size — how many recent events are retained per process for replay. Env `MCP_SSE_MAX_STORED_EVENTS`. |
|
|
495
|
+
|
|
496
|
+
The store is a per-process ring buffer: it does not survive a restart and is not shared across
|
|
497
|
+
instances. For multi-replica deployments either pin reconnects to the same instance (sticky sessions
|
|
498
|
+
by `Mcp-Session-Id`) or implement a shared `EventStore`. Events evicted past `maxStoredEvents` are not
|
|
499
|
+
replayed — the client simply resumes from the current moment, without error.
|
|
500
|
+
|
|
501
|
+
## HTTP Transport Hardening (`mcp.limits`)
|
|
502
|
+
|
|
503
|
+
Standard §14 mandates explicit ceilings on request body, tool result and tool execution time. The
|
|
504
|
+
SDK enforces all three from `mcp.limits` — see the snippet under "config/default.yaml" above.
|
|
505
|
+
|
|
506
|
+
| Key | Default | What happens above the limit |
|
|
507
|
+
|-----|---------|------------------------------|
|
|
508
|
+
| `mcp.limits.maxPayloadBytes` | 1 MiB | JSON-RPC `-32005` + HTTP **413 Payload Too Large**. The Express `entity.too.large` error is translated automatically — clients never see the default HTML error page. |
|
|
509
|
+
| `mcp.limits.maxToolResultBytes` | 10 MiB | Response is truncated. `structuredContent.truncated: true` is set on structured payloads; `…[truncated]` marker is appended to oversized text content. Standard §12.2. |
|
|
510
|
+
| `mcp.limits.toolTimeoutMs` | 30 000 ms | JSON-RPC `-32004` + HTTP **504 Gateway Timeout** on `/mcp`. The pending tool promise is left running (Node can't synchronously abort user code); your tool SHOULD also self-cancel if it watches the elapsed time. |
|
|
511
|
+
|
|
512
|
+
Override per-environment in `config/{development,production,local}.yaml` or via env vars
|
|
513
|
+
(`MCP_LIMITS_MAX_PAYLOAD_BYTES`, `MCP_LIMITS_MAX_TOOL_RESULT_BYTES`, `MCP_LIMITS_TOOL_TIMEOUT_MS`).
|
|
514
|
+
|
|
515
|
+
## Health, Readiness, CORS
|
|
516
|
+
|
|
517
|
+
| Endpoint / Setting | Behaviour | Standard |
|
|
518
|
+
|--------------------|-----------|----------|
|
|
519
|
+
| `GET /health` | Returns `{ status, version, uptime, details }`. HTTP **503** when `status === 'unhealthy'`, **200** otherwise. | §16.1 |
|
|
520
|
+
| `GET /ready` | No auth. Returns `{ status, checks: { db, cache, jwks } }`. Each check is `'ok' \| 'error' \| 'skipped'` — never leaks credentials or connection strings. HTTP **503** when any check fails, **200** when all green. | §16.2 / §16.3 |
|
|
521
|
+
| `webServer.host` | Default `'127.0.0.1'` (loopback). Containers / k8s pods / public-facing deployments MUST set `'0.0.0.0'` explicitly. | §6 |
|
|
522
|
+
| `webServer.originHosts` | Empty list in production aborts `initMcpServer()`. Unlisted `Origin` headers receive HTTP **403** + JSON-RPC error (no longer silently allowed). | §6 |
|
|
523
|
+
|
|
524
|
+
## MCP-Specific JSON-RPC Error Codes (Appendix B)
|
|
525
|
+
|
|
526
|
+
| Code | Class | HTTP | When |
|
|
527
|
+
|------|-------|------|------|
|
|
528
|
+
| `-32002` | `ResourceNotFoundError` | 404 | Session / resource not found (legacy SSE `/messages`, missing JWKS key, etc.) |
|
|
529
|
+
| `-32003` | `RateLimitedError` | 429 | Per-client rate limit exceeded. Response carries the `Retry-After` HTTP header AND `error.data.retryAfter` (seconds). |
|
|
530
|
+
| `-32004` | `TimeoutError` | 504 | Tool execution exceeded `mcp.limits.toolTimeoutMs`. |
|
|
531
|
+
| `-32005` | `PayloadTooLargeError` | 413 | Request body exceeded `mcp.limits.maxPayloadBytes`. |
|
|
532
|
+
|
|
533
|
+
Import the classes (and the `MCP_ERROR_CODES` map) from the SDK root:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
import {
|
|
537
|
+
PayloadTooLargeError, TimeoutError, RateLimitedError, ResourceNotFoundError,
|
|
538
|
+
MCP_ERROR_CODES,
|
|
539
|
+
createJsonRpcErrorResponse, // accepts (err, requestId?, extraData?)
|
|
540
|
+
IMcpErrorData, // { requestId?, field?, reason?, retryAfter?, [k]: unknown }
|
|
541
|
+
} from 'fa-mcp-sdk';
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
All four extend `BaseMcpError`. The `createJsonRpcErrorResponse` helper emits the canonical
|
|
545
|
+
`error.data` shape from Appendix B.3 — `{ requestId?, field?, reason?, retryAfter?, … }`.
|
|
546
|
+
Stack traces and internal paths are NEVER included in `error.data` (standard §13.3).
|
|
547
|
+
|
|
@@ -12,16 +12,83 @@ interface AuthResult {
|
|
|
12
12
|
username?: string;
|
|
13
13
|
isTokenDecrypted?: boolean;
|
|
14
14
|
payload?: any;
|
|
15
|
+
/**
|
|
16
|
+
* Standard §7.4 — authenticated but not authorized. Triggers HTTP 403
|
|
17
|
+
* (NO WWW-Authenticate challenge). Set by custom validators or scope checks.
|
|
18
|
+
*/
|
|
19
|
+
forbidden?: boolean;
|
|
15
20
|
}
|
|
16
21
|
```
|
|
17
22
|
|
|
23
|
+
## JWT Modes (since SDK 0.7.0)
|
|
24
|
+
|
|
25
|
+
`webServer.auth.jwtToken.mode` selects the JWT engine:
|
|
26
|
+
|
|
27
|
+
| Mode | Algorithm | Issues tokens | Verifies tokens | Discovery | Use case |
|
|
28
|
+
|------|-----------|---------------|-----------------|-----------|----------|
|
|
29
|
+
| `legacyAesCtr` (default) | HS256 + legacy AES-CTR | yes (HS256) | yes | — | Backward-compatible / 0.6.x parity |
|
|
30
|
+
| `embedded` | ES256 / RS256 | yes (autogen keys) | yes | full OIDC + JWKS + `/oauth/token` | Dev / demo |
|
|
31
|
+
| `localKey` | ES256 / RS256 | when `privateKeyPath` set | yes | OIDC + JWKS | Isolated server with PEM keys |
|
|
32
|
+
| `remoteJwks` | ES256 / RS256 | NO (`501`) | yes (remote JWKS) | protected-resource only | Corporate IdP (Keycloak, Okta, Azure AD, …) |
|
|
33
|
+
|
|
34
|
+
Pre-flight checks (`init-mcp-server.ts`) reject misconfigured non-legacy modes at start:
|
|
35
|
+
|
|
36
|
+
- `remoteJwks` without `jwksUri` → throws
|
|
37
|
+
- `localKey` without `publicKeyPath` → throws
|
|
38
|
+
- non-legacy without `expectedIssuer` → throws (standard §7.2)
|
|
39
|
+
- `clockSkew > 60s` → throws (standard Прил. A.1)
|
|
40
|
+
- `production` + `legacyAesCtr` + `auth.enabled=true` → warn (asymmetric required by standard)
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
# config/local.yaml — corporate IdP
|
|
44
|
+
webServer:
|
|
45
|
+
trustProxy: true # behind HTTPS reverse proxy
|
|
46
|
+
auth:
|
|
47
|
+
enabled: true
|
|
48
|
+
jwtToken:
|
|
49
|
+
mode: remoteJwks
|
|
50
|
+
jwksUri: 'https://idp.corp/.well-known/jwks.json'
|
|
51
|
+
expectedIssuer: 'https://idp.corp'
|
|
52
|
+
expectedAudience: '${SERVICE_NAME}'
|
|
53
|
+
jwksCacheTtl: 600
|
|
54
|
+
jwksCooldown: 30
|
|
55
|
+
clockSkew: 30
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Discovery endpoints mounted automatically when `mode != 'legacyAesCtr'`:
|
|
59
|
+
|
|
60
|
+
- `GET /.well-known/oauth-protected-resource` (any non-legacy)
|
|
61
|
+
- `GET /.well-known/openid-configuration` (`embedded` / `localKey`)
|
|
62
|
+
- `GET /.well-known/jwks.json` (`embedded` / `localKey`)
|
|
63
|
+
- `POST /oauth/token` (`embedded` + `localKey` with private key, `grant_type=password`)
|
|
64
|
+
|
|
65
|
+
On every 401 the server sets:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
WWW-Authenticate: Bearer realm="<appConfig.name>",
|
|
69
|
+
resource_metadata="<base>/.well-known/oauth-protected-resource"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If the token was decoded but rejected (expired, bad scope), the header additionally carries
|
|
73
|
+
`error="invalid_token", error_description="…"` per RFC 6750.
|
|
74
|
+
|
|
18
75
|
## Token Operations
|
|
19
76
|
|
|
20
77
|
```typescript
|
|
21
|
-
import { generateToken } from 'fa-mcp-sdk';
|
|
78
|
+
import { generateToken, checkJwtToken } from 'fa-mcp-sdk';
|
|
79
|
+
|
|
80
|
+
// Since 0.7.0 — generateToken / checkJwtToken are async (dispatch by jwtToken.mode).
|
|
81
|
+
const token = await generateToken('john_doe', 3600, { role: 'admin' }); // 1 hour
|
|
82
|
+
const r = await checkJwtToken({ token });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Synchronous fallbacks (legacy mode only — never throw on `mode = legacyAesCtr`):
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { generateTokenLegacy, checkJwtTokenLegacy } from 'fa-mcp-sdk';
|
|
22
89
|
|
|
23
|
-
|
|
24
|
-
const
|
|
90
|
+
const token = generateTokenLegacy('john_doe', 3600, { role: 'admin' });
|
|
91
|
+
const r = checkJwtTokenLegacy({ token });
|
|
25
92
|
```
|
|
26
93
|
|
|
27
94
|
## Test Authentication
|
|
@@ -29,8 +96,9 @@ const token = generateToken('john_doe', 3600, { role: 'admin' }); // 1 hour
|
|
|
29
96
|
```typescript
|
|
30
97
|
import { getAuthHeadersForTests, McpHttpClient, appConfig } from 'fa-mcp-sdk';
|
|
31
98
|
|
|
32
|
-
//
|
|
33
|
-
|
|
99
|
+
// Since 0.7.0 — getAuthHeadersForTests is async. Uses canLocallyIssueJwt() so JWT-based
|
|
100
|
+
// headers work in every mode that can sign locally (legacy / embedded / localKey).
|
|
101
|
+
const headers = await getAuthHeadersForTests();
|
|
34
102
|
|
|
35
103
|
// Usage
|
|
36
104
|
const response = await fetch(`http://localhost:${appConfig.webServer.port}/mcp`, {
|
|
@@ -41,7 +109,62 @@ const response = await fetch(`http://localhost:${appConfig.webServer.port}/mcp`,
|
|
|
41
109
|
|
|
42
110
|
// With test client
|
|
43
111
|
const client = new McpHttpClient('http://localhost:3000');
|
|
44
|
-
const result = await client.callTool('tool', args, getAuthHeadersForTests());
|
|
112
|
+
const result = await client.callTool('tool', args, await getAuthHeadersForTests());
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Scope Enforcement (since SDK 0.7.0)
|
|
116
|
+
|
|
117
|
+
Standard §7.5 — protect specific tools / prompts / resources with `requiredScopes`. The auth
|
|
118
|
+
middleware checks the token's `scope` claim (space-separated) against the declared list and
|
|
119
|
+
returns HTTP 403 (or JSON-RPC `-32004` for tools) when scopes are missing.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Resource with required scope
|
|
123
|
+
{
|
|
124
|
+
uri: 'admin://users',
|
|
125
|
+
name: 'users',
|
|
126
|
+
description: 'Admin user list',
|
|
127
|
+
mimeType: 'application/json',
|
|
128
|
+
requireAuth: true,
|
|
129
|
+
requiredScopes: ['mcp:admin'],
|
|
130
|
+
content: async () => ({ ... }),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Prompt with required scope
|
|
134
|
+
{
|
|
135
|
+
name: 'admin_brief',
|
|
136
|
+
description: 'Privileged brief',
|
|
137
|
+
arguments: [],
|
|
138
|
+
content: '…',
|
|
139
|
+
requiredScopes: ['mcp:admin'],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Tool with required scope (via _meta — SDK Tool type has no native scope field)
|
|
143
|
+
{
|
|
144
|
+
name: 'delete_user',
|
|
145
|
+
description: 'Delete a user account',
|
|
146
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
147
|
+
_meta: { requiredScopes: ['mcp:admin'] },
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A token issued via `POST /oauth/token` with `scope=mcp:admin` (or any IdP token carrying that
|
|
152
|
+
scope) passes; everything else hits 403. The full server-side scope map is published via the
|
|
153
|
+
built-in `use://auth` resource — clients can introspect it programmatically.
|
|
154
|
+
|
|
155
|
+
## Forbidden vs Unauthorized
|
|
156
|
+
|
|
157
|
+
Custom validators can now signal 403 explicitly via `AuthResult.forbidden`:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const validator: CustomAuthValidator = async (req) => {
|
|
161
|
+
const token = req.headers['x-api-key'];
|
|
162
|
+
if (!token) return { success: false, error: 'No API key' }; // → 401
|
|
163
|
+
if (await tokenIsRevoked(token)) {
|
|
164
|
+
return { success: false, forbidden: true, error: 'Token revoked' }; // → 403, no challenge
|
|
165
|
+
}
|
|
166
|
+
return { success: true, authType: 'custom' };
|
|
167
|
+
};
|
|
45
168
|
```
|
|
46
169
|
|
|
47
170
|
## Admin Panel Authentication
|
|
@@ -296,12 +419,31 @@ curl -H "Authorization: Basic $(echo -n 'admin:password' | base64)" http://local
|
|
|
296
419
|
curl -H "X-API-Key: custom-key" http://localhost:3000/mcp
|
|
297
420
|
```
|
|
298
421
|
|
|
422
|
+
## Token Check Endpoint (`/ct`)
|
|
423
|
+
|
|
424
|
+
Standard §7.1 forbids secrets in URL query strings. Since SDK 0.7.0 `GET /ct?t=<token>` is
|
|
425
|
+
disabled by default and returns HTTP 405. Use `POST /ct` with JSON body instead:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
curl -X POST http://localhost:3000/ct \
|
|
429
|
+
-H "Content-Type: application/json" \
|
|
430
|
+
-d '{"t": "<your-token>"}'
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Opt-in for the legacy form (non-production only — flag is ignored when `NODE_ENV=production`):
|
|
434
|
+
|
|
435
|
+
```yaml
|
|
436
|
+
webServer:
|
|
437
|
+
tokenCheck:
|
|
438
|
+
allowQueryToken: true
|
|
439
|
+
```
|
|
440
|
+
|
|
299
441
|
## CLI Token Generator
|
|
300
442
|
|
|
301
443
|
Generate JWT tokens from the command line without starting the server:
|
|
302
444
|
|
|
303
445
|
```bash
|
|
304
|
-
node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <params>]
|
|
446
|
+
node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <params>] [--key <path>]
|
|
305
447
|
```
|
|
306
448
|
|
|
307
449
|
| Option | ENV | Description |
|
|
@@ -310,8 +452,16 @@ node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <p
|
|
|
310
452
|
| `-ttl` | `JWT_TTL` | Token lifetime: `<N>s` \| `<N>m` \| `<N>d` \| `<N>y` (required) |
|
|
311
453
|
| `-s`, `--service-name` | `JWT_PAYLOAD_SERVICE_NAME` | Service name (optional) |
|
|
312
454
|
| `-p`, `--params` | `JWT_PAYLOAD_PARAMS` | Extra payload `key=value;key=value` (optional) |
|
|
455
|
+
| `--key`, `--private-key` | — | Override private key path (only for embedded / localKey modes) |
|
|
456
|
+
|
|
457
|
+
Behaviour by `webServer.auth.jwtToken.mode`:
|
|
313
458
|
|
|
314
|
-
|
|
459
|
+
| Mode | What the script does |
|
|
460
|
+
|------|----------------------|
|
|
461
|
+
| `legacyAesCtr` | HS256 with `encryptKey` (legacy). |
|
|
462
|
+
| `embedded` | ES256/RS256 with keys from `keyStoragePath/private.pem`. |
|
|
463
|
+
| `localKey` | ES256/RS256 with `privateKeyPath` (must be configured or passed via `--key`). |
|
|
464
|
+
| `remoteJwks` | Exits with error — tokens must be obtained from the external IdP. |
|
|
315
465
|
|
|
316
466
|
**Examples:**
|
|
317
467
|
|