@namzu/sdk 3.0.0 → 3.2.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/CHANGELOG.md +223 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
- package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
- package/dist/advisory/__tests__/consultation-context.test.js +124 -0
- package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
- package/dist/advisory/context.d.ts +25 -0
- package/dist/advisory/context.d.ts.map +1 -1
- package/dist/advisory/context.js +18 -0
- package/dist/advisory/context.js.map +1 -1
- package/dist/advisory/executor.d.ts.map +1 -1
- package/dist/advisory/executor.js +25 -3
- package/dist/advisory/executor.js.map +1 -1
- package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
- package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
- package/dist/compaction/__tests__/context-reducer.test.js +197 -0
- package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
- package/dist/compaction/factory.d.ts +7 -0
- package/dist/compaction/factory.d.ts.map +1 -1
- package/dist/compaction/factory.js +7 -0
- package/dist/compaction/factory.js.map +1 -1
- package/dist/compaction/index.d.ts +2 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js +1 -0
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/interface.d.ts +13 -0
- package/dist/compaction/interface.d.ts.map +1 -1
- package/dist/compaction/managers/null.d.ts +3 -0
- package/dist/compaction/managers/null.d.ts.map +1 -1
- package/dist/compaction/managers/null.js +3 -0
- package/dist/compaction/managers/null.js.map +1 -1
- package/dist/compaction/managers/slidingWindow.d.ts +6 -0
- package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
- package/dist/compaction/managers/slidingWindow.js +6 -0
- package/dist/compaction/managers/slidingWindow.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts +10 -0
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +10 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/reducer.d.ts +86 -0
- package/dist/compaction/reducer.d.ts.map +1 -0
- package/dist/compaction/reducer.js +77 -0
- package/dist/compaction/reducer.js.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
- package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +24 -2
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/http.test.js +18 -2
- package/dist/connector/builtins/http.test.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
- package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +52 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +86 -0
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +12 -1
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +19 -4
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +2 -2
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -1
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/server.d.ts +42 -1
- package/dist/connector/mcp/server.d.ts.map +1 -1
- package/dist/connector/mcp/server.js +77 -4
- package/dist/connector/mcp/server.js.map +1 -1
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
- package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
- package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
- package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
- package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
- package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
- package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
- package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
- package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts +41 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +29 -1
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts +39 -3
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +37 -4
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +11 -3
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
- package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
- package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
- package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
- package/dist/rag/ingestion.d.ts.map +1 -1
- package/dist/rag/ingestion.js +1 -0
- package/dist/rag/ingestion.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/retriever.js +2 -0
- package/dist/rag/retriever.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js +6 -0
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +113 -109
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
- package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
- package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
- package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +10 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +10 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +26 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +118 -34
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
- package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +95 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
- package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +70 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -0
- package/dist/runtime/query/resume-run.js +46 -0
- package/dist/runtime/query/resume-run.js.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
- package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
- package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
- package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
- package/dist/telemetry/__tests__/span-closure.test.js +124 -0
- package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
- package/dist/tools/advisory/index.js +1 -1
- package/dist/tools/advisory/index.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
- package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
- package/dist/tools/builtins/__tests__/edit.test.js +242 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
- package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
- package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
- package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
- package/dist/tools/builtins/atomic-write-file.js +31 -2
- package/dist/tools/builtins/atomic-write-file.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
- package/dist/tools/builtins/content-fingerprint.js +30 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +172 -31
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +5 -2
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +75 -27
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
- package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
- package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +75 -5
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
- package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
- package/dist/tools/coordinator/plan-dependencies.js +148 -0
- package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
- package/dist/types/agent/supervisor.d.ts +15 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/connector/core.d.ts +34 -0
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +10 -0
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +13 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/rag/retrieval.d.ts +16 -0
- package/dist/types/rag/retrieval.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +9 -0
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +11 -0
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/types/router/task-router.d.ts +19 -0
- package/dist/types/router/task-router.d.ts.map +1 -1
- package/dist/types/run/prepare-step.d.ts +56 -3
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +17 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/toolset/index.d.ts +22 -0
- package/dist/types/toolset/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/advisory/__tests__/consultation-context.test.ts +191 -0
- package/src/advisory/context.ts +32 -0
- package/src/advisory/executor.ts +30 -3
- package/src/compaction/__tests__/context-reducer.test.ts +239 -0
- package/src/compaction/factory.ts +7 -0
- package/src/compaction/index.ts +8 -0
- package/src/compaction/interface.ts +13 -0
- package/src/compaction/managers/null.ts +3 -0
- package/src/compaction/managers/slidingWindow.ts +6 -0
- package/src/compaction/managers/structured.ts +10 -0
- package/src/compaction/reducer.ts +154 -0
- package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
- package/src/connector/builtins/http.test.ts +28 -2
- package/src/connector/builtins/http.ts +26 -2
- package/src/connector/index.ts +6 -2
- package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
- package/src/connector/mcp/client.ts +95 -0
- package/src/connector/mcp/discovery.ts +19 -4
- package/src/connector/mcp/index.ts +6 -2
- package/src/connector/mcp/server.ts +101 -3
- package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
- package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
- package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
- package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
- package/src/plugin/lifecycle.ts +57 -1
- package/src/plugin/loader.ts +57 -3
- package/src/public-runtime.ts +16 -1
- package/src/public-types.ts +5 -0
- package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
- package/src/rag/ingestion.ts +1 -0
- package/src/rag/retriever.ts +2 -0
- package/src/rag/vector-store.ts +5 -0
- package/src/registry/tool/execute.ts +123 -119
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
- package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
- package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
- package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
- package/src/runtime/query/executor.ts +9 -1
- package/src/runtime/query/index.ts +39 -0
- package/src/runtime/query/iteration/index.ts +130 -34
- package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
- package/src/runtime/query/iteration/phases/compaction.ts +106 -5
- package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/resume-run.ts +93 -0
- package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
- package/src/telemetry/__tests__/span-closure.test.ts +153 -0
- package/src/tools/advisory/index.ts +1 -1
- package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
- package/src/tools/builtins/__tests__/edit.test.ts +324 -1
- package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
- package/src/tools/builtins/atomic-write-file.ts +30 -2
- package/src/tools/builtins/content-fingerprint.ts +33 -0
- package/src/tools/builtins/edit.ts +188 -36
- package/src/tools/builtins/read-file.ts +5 -2
- package/src/tools/builtins/write-file.ts +79 -28
- package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
- package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
- package/src/tools/coordinator/index.ts +116 -42
- package/src/tools/coordinator/plan-dependencies.ts +175 -0
- package/src/types/agent/supervisor.ts +15 -0
- package/src/types/connector/core.ts +34 -0
- package/src/types/connector/definition.ts +10 -0
- package/src/types/connector/mcp.ts +14 -0
- package/src/types/rag/retrieval.ts +16 -0
- package/src/types/rag/storage.ts +9 -0
- package/src/types/rag/vector.ts +11 -0
- package/src/types/router/task-router.ts +19 -0
- package/src/types/run/prepare-step.ts +58 -3
- package/src/types/tool/index.ts +17 -1
- package/src/types/toolset/index.ts +22 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { JSON_RPC_METHOD_NOT_FOUND } from '../../../constants/mcp/index.js'
|
|
4
|
+
import type {
|
|
5
|
+
MCPJsonRpcMessage,
|
|
6
|
+
MCPPromptDefinition,
|
|
7
|
+
MCPPromptMessage,
|
|
8
|
+
MCPTransport,
|
|
9
|
+
} from '../../../types/connector/index.js'
|
|
10
|
+
import { MCPClient } from '../client.js'
|
|
11
|
+
import { MCPServer } from '../server.js'
|
|
12
|
+
import type { MCPServerPromptProvider, MCPServerToolProvider } from '../server.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The half of MCP that is not tools.
|
|
16
|
+
*
|
|
17
|
+
* `MCPPromptDefinition` and `MCPPromptArgument` were declared when the types
|
|
18
|
+
* were written; no client method ever asked for a prompt and no server branch
|
|
19
|
+
* ever served one. `MCPLifecycleEvent` and `MCPEventListener` were declared
|
|
20
|
+
* beside them and nothing ever emitted one, so a host could learn that a
|
|
21
|
+
* server had died only by noticing calls had started failing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function fakeTransport(): {
|
|
25
|
+
transport: MCPTransport
|
|
26
|
+
sent: MCPJsonRpcMessage[]
|
|
27
|
+
deliver: (m: MCPJsonRpcMessage) => void
|
|
28
|
+
} {
|
|
29
|
+
const sent: MCPJsonRpcMessage[] = []
|
|
30
|
+
let onMessage: ((m: MCPJsonRpcMessage) => void) | undefined
|
|
31
|
+
const transport: MCPTransport = {
|
|
32
|
+
connect: async () => undefined,
|
|
33
|
+
close: async () => undefined,
|
|
34
|
+
send: async (m: MCPJsonRpcMessage) => {
|
|
35
|
+
sent.push(m)
|
|
36
|
+
},
|
|
37
|
+
onMessage: (h: (m: MCPJsonRpcMessage) => void) => {
|
|
38
|
+
onMessage = h
|
|
39
|
+
},
|
|
40
|
+
onClose: () => undefined,
|
|
41
|
+
onError: () => undefined,
|
|
42
|
+
} as unknown as MCPTransport
|
|
43
|
+
return { transport, sent, deliver: (m) => onMessage?.(m) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const toolProvider: MCPServerToolProvider = {
|
|
47
|
+
listTools: () => [],
|
|
48
|
+
callTool: async () => ({ content: [{ type: 'text', text: 'ok' }] }),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function promptProvider(over: Partial<MCPServerPromptProvider> = {}): MCPServerPromptProvider {
|
|
52
|
+
const prompts: MCPPromptDefinition[] = [
|
|
53
|
+
{
|
|
54
|
+
name: 'summarize',
|
|
55
|
+
description: 'Summarize a file',
|
|
56
|
+
arguments: [
|
|
57
|
+
{ name: 'path', required: true },
|
|
58
|
+
{ name: 'style', required: false },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
const messages: MCPPromptMessage[] = [
|
|
63
|
+
{ role: 'user', content: { type: 'text', text: 'summarize it' } },
|
|
64
|
+
]
|
|
65
|
+
return {
|
|
66
|
+
listPrompts: () => prompts,
|
|
67
|
+
getPrompt: async () => ({ description: 'Summarize a file', messages }),
|
|
68
|
+
...over,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function ask(
|
|
73
|
+
t: ReturnType<typeof fakeTransport>,
|
|
74
|
+
method: string,
|
|
75
|
+
params: Record<string, unknown> = {},
|
|
76
|
+
) {
|
|
77
|
+
t.deliver({ jsonrpc: '2.0', id: 1, method, params } as MCPJsonRpcMessage)
|
|
78
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
79
|
+
return t.sent.at(-1) as MCPJsonRpcMessage & {
|
|
80
|
+
result?: unknown
|
|
81
|
+
error?: { code: number; message: string }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('a server serves the prompts it publishes', () => {
|
|
86
|
+
it('lists them', async () => {
|
|
87
|
+
const t = fakeTransport()
|
|
88
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
|
|
89
|
+
await server.start(t.transport)
|
|
90
|
+
|
|
91
|
+
const reply = await ask(t, 'prompts/list')
|
|
92
|
+
|
|
93
|
+
expect((reply.result as { prompts: MCPPromptDefinition[] }).prompts[0]?.name).toBe('summarize')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns the messages the server composed', async () => {
|
|
97
|
+
const t = fakeTransport()
|
|
98
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
|
|
99
|
+
await server.start(t.transport)
|
|
100
|
+
|
|
101
|
+
const reply = await ask(t, 'prompts/get', {
|
|
102
|
+
name: 'summarize',
|
|
103
|
+
arguments: { path: 'a.ts' },
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect((reply.result as { messages: MCPPromptMessage[] }).messages[0]?.content).toEqual({
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: 'summarize it',
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('advertises the capability only when something is behind it', async () => {
|
|
113
|
+
const withPrompts = fakeTransport()
|
|
114
|
+
const without = fakeTransport()
|
|
115
|
+
const a = new MCPServer({ name: 'a' }, toolProvider, undefined, promptProvider())
|
|
116
|
+
const b = new MCPServer({ name: 'b' }, toolProvider)
|
|
117
|
+
await a.start(withPrompts.transport)
|
|
118
|
+
await b.start(without.transport)
|
|
119
|
+
|
|
120
|
+
const withCap = await ask(withPrompts, 'initialize')
|
|
121
|
+
const withoutCap = await ask(without, 'initialize')
|
|
122
|
+
|
|
123
|
+
// A capability advertised without a provider is a promise the next
|
|
124
|
+
// call breaks.
|
|
125
|
+
expect(
|
|
126
|
+
(withCap.result as { capabilities: Record<string, unknown> }).capabilities.prompts,
|
|
127
|
+
).toBeDefined()
|
|
128
|
+
expect(
|
|
129
|
+
(withoutCap.result as { capabilities: Record<string, unknown> }).capabilities.prompts,
|
|
130
|
+
).toBeUndefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('refuses a required argument the caller omitted', async () => {
|
|
134
|
+
const t = fakeTransport()
|
|
135
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
|
|
136
|
+
await server.start(t.transport)
|
|
137
|
+
|
|
138
|
+
const reply = await ask(t, 'prompts/get', { name: 'summarize', arguments: {} })
|
|
139
|
+
|
|
140
|
+
// Checked from the declaration, so every provider does not have to
|
|
141
|
+
// re-implement it or forget to.
|
|
142
|
+
expect(reply.error?.message).toContain('path')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('accepts an optional argument being absent', async () => {
|
|
146
|
+
const t = fakeTransport()
|
|
147
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
|
|
148
|
+
await server.start(t.transport)
|
|
149
|
+
|
|
150
|
+
const reply = await ask(t, 'prompts/get', {
|
|
151
|
+
name: 'summarize',
|
|
152
|
+
arguments: { path: 'a.ts' },
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(reply.error).toBeUndefined()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('refuses a prompt it does not publish', async () => {
|
|
159
|
+
const t = fakeTransport()
|
|
160
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider, undefined, promptProvider())
|
|
161
|
+
await server.start(t.transport)
|
|
162
|
+
|
|
163
|
+
const reply = await ask(t, 'prompts/get', { name: 'nonexistent' })
|
|
164
|
+
|
|
165
|
+
expect(reply.error?.message).toContain('nonexistent')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('"none" and "not here" are different answers', () => {
|
|
170
|
+
it('says method-not-found for prompts when there is no provider', async () => {
|
|
171
|
+
const t = fakeTransport()
|
|
172
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider)
|
|
173
|
+
await server.start(t.transport)
|
|
174
|
+
|
|
175
|
+
const reply = await ask(t, 'prompts/list')
|
|
176
|
+
|
|
177
|
+
expect(reply.error?.code).toBe(JSON_RPC_METHOD_NOT_FOUND)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('says method-not-found for resources when there is no provider', async () => {
|
|
181
|
+
const t = fakeTransport()
|
|
182
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider)
|
|
183
|
+
await server.start(t.transport)
|
|
184
|
+
|
|
185
|
+
const reply = await ask(t, 'resources/list')
|
|
186
|
+
|
|
187
|
+
// This used to answer `{ resources: [] }` for a capability
|
|
188
|
+
// `initialize` never advertised — telling a client "none" where the
|
|
189
|
+
// truth is "not here". The two send a client in opposite directions.
|
|
190
|
+
expect(reply.error?.code).toBe(JSON_RPC_METHOD_NOT_FOUND)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('uses the protocol code for an unknown method, not a generic failure', async () => {
|
|
194
|
+
const t = fakeTransport()
|
|
195
|
+
const server = new MCPServer({ name: 'srv' }, toolProvider)
|
|
196
|
+
await server.start(t.transport)
|
|
197
|
+
|
|
198
|
+
const reply = await ask(t, 'completely/unknown')
|
|
199
|
+
|
|
200
|
+
expect(reply.error?.code).toBe(JSON_RPC_METHOD_NOT_FOUND)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('still reports a provider failure as an internal error', async () => {
|
|
204
|
+
const t = fakeTransport()
|
|
205
|
+
const server = new MCPServer(
|
|
206
|
+
{ name: 'srv' },
|
|
207
|
+
toolProvider,
|
|
208
|
+
undefined,
|
|
209
|
+
promptProvider({
|
|
210
|
+
listPrompts: () => {
|
|
211
|
+
throw new Error('provider exploded')
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
await server.start(t.transport)
|
|
216
|
+
|
|
217
|
+
const reply = await ask(t, 'prompts/list')
|
|
218
|
+
|
|
219
|
+
// A broken provider is not the same as an unimplemented method, and
|
|
220
|
+
// collapsing the two would tell a client to stop asking for something
|
|
221
|
+
// that works tomorrow.
|
|
222
|
+
expect(reply.error?.code).toBe(-32603)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('a client says out loud what it already knew', () => {
|
|
227
|
+
function failingClient() {
|
|
228
|
+
// `connect` failing is the simplest path that actually emits. A
|
|
229
|
+
// never-connected client's `disconnect` returns early and emits
|
|
230
|
+
// nothing — an easy way to write a test that proves nothing, which is
|
|
231
|
+
// what the first version of these did.
|
|
232
|
+
return new MCPClient({
|
|
233
|
+
serverName: 'srv',
|
|
234
|
+
transport: { type: 'stdio', command: 'definitely-not-a-real-binary-xyz', args: [] },
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Covered here: the transport-error emitter. `connect`'s own catch emits
|
|
240
|
+
* the same event and is NOT pinned — a spawn failure surfaces through the
|
|
241
|
+
* transport first, so the catch is the second line of defence rather than
|
|
242
|
+
* the path this reaches. Reaching it needs a connection that opens and
|
|
243
|
+
* then fails negotiation, which this fake cannot stage; said rather than
|
|
244
|
+
* left for a mutation run to discover.
|
|
245
|
+
*/
|
|
246
|
+
it('reports a failure to whoever is listening', async () => {
|
|
247
|
+
const client = failingClient()
|
|
248
|
+
const seen: string[] = []
|
|
249
|
+
client.onLifecycle((e) => seen.push(e.type))
|
|
250
|
+
|
|
251
|
+
await client.connect().catch(() => undefined)
|
|
252
|
+
|
|
253
|
+
expect(seen).toContain('mcp_client_error')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('stops reporting once the listener unsubscribes', async () => {
|
|
257
|
+
const client = failingClient()
|
|
258
|
+
const seen = vi.fn()
|
|
259
|
+
|
|
260
|
+
const off = client.onLifecycle(seen)
|
|
261
|
+
off()
|
|
262
|
+
await client.connect().catch(() => undefined)
|
|
263
|
+
|
|
264
|
+
// `onNotification` returns nothing, so a listener registered there
|
|
265
|
+
// cannot be removed and keeps a disposed host object alive for as
|
|
266
|
+
// long as the client. This is that bug not repeated.
|
|
267
|
+
expect(seen).not.toHaveBeenCalled()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('leaves other listeners working when one throws', async () => {
|
|
271
|
+
const client = failingClient()
|
|
272
|
+
const survivor = vi.fn()
|
|
273
|
+
client.onLifecycle(() => {
|
|
274
|
+
throw new Error('observer bug')
|
|
275
|
+
})
|
|
276
|
+
client.onLifecycle(survivor)
|
|
277
|
+
|
|
278
|
+
await client.connect().catch(() => undefined)
|
|
279
|
+
|
|
280
|
+
// These fire from inside transport callbacks and from `connect`'s
|
|
281
|
+
// failure path, so an escaping exception would surface as a connection
|
|
282
|
+
// error — blaming the server for a bug in the host's own observer, and
|
|
283
|
+
// silencing every listener registered after the broken one.
|
|
284
|
+
expect(survivor).toHaveBeenCalled()
|
|
285
|
+
})
|
|
286
|
+
})
|
|
@@ -3,8 +3,12 @@ import type {
|
|
|
3
3
|
MCPClientState,
|
|
4
4
|
MCPConnectionStatus,
|
|
5
5
|
MCPContentBlock,
|
|
6
|
+
MCPEventListener,
|
|
6
7
|
MCPInitializeResult,
|
|
7
8
|
MCPJsonRpcMessage,
|
|
9
|
+
MCPLifecycleEvent,
|
|
10
|
+
MCPPromptDefinition,
|
|
11
|
+
MCPPromptMessage,
|
|
8
12
|
MCPResource,
|
|
9
13
|
MCPResourceTemplate,
|
|
10
14
|
MCPServerCapabilities,
|
|
@@ -52,6 +56,7 @@ export class MCPClient {
|
|
|
52
56
|
private nextRequestId = 1
|
|
53
57
|
private notificationHandlers: Array<(method: string, params?: Record<string, unknown>) => void> =
|
|
54
58
|
[]
|
|
59
|
+
private lifecycleListeners: MCPEventListener[] = []
|
|
55
60
|
private log: Logger
|
|
56
61
|
private readonly config: MCPClientConfig
|
|
57
62
|
|
|
@@ -74,12 +79,14 @@ export class MCPClient {
|
|
|
74
79
|
this.transport.onClose(() => {
|
|
75
80
|
this.status = 'disconnected'
|
|
76
81
|
this.log.info('MCP transport closed')
|
|
82
|
+
this.emitLifecycle({ type: 'mcp_client_disconnected', clientId: this.id })
|
|
77
83
|
this.rejectAllPending(`MCP transport to "${this.config.serverName}" closed`)
|
|
78
84
|
})
|
|
79
85
|
this.transport.onError((err) => {
|
|
80
86
|
this.status = 'error'
|
|
81
87
|
this.error = err.message
|
|
82
88
|
this.log.error('MCP transport error', { error: err.message })
|
|
89
|
+
this.emitLifecycle({ type: 'mcp_client_error', clientId: this.id, error: err.message })
|
|
83
90
|
this.rejectAllPending(`MCP transport to "${this.config.serverName}" failed: ${err.message}`)
|
|
84
91
|
})
|
|
85
92
|
|
|
@@ -116,6 +123,11 @@ export class MCPClient {
|
|
|
116
123
|
|
|
117
124
|
this.status = 'connected'
|
|
118
125
|
this.connectedAt = Date.now()
|
|
126
|
+
this.emitLifecycle({
|
|
127
|
+
type: 'mcp_client_connected',
|
|
128
|
+
clientId: this.id,
|
|
129
|
+
serverName: this.config.serverName,
|
|
130
|
+
})
|
|
119
131
|
this.log.info(`Connected to MCP server: ${result.serverInfo.name}`)
|
|
120
132
|
|
|
121
133
|
return result
|
|
@@ -123,6 +135,7 @@ export class MCPClient {
|
|
|
123
135
|
this.status = 'error'
|
|
124
136
|
this.error = toErrorMessage(err)
|
|
125
137
|
this.log.error('MCP connection failed', { error: this.error })
|
|
138
|
+
this.emitLifecycle({ type: 'mcp_client_error', clientId: this.id, error: this.error })
|
|
126
139
|
throw err
|
|
127
140
|
}
|
|
128
141
|
}
|
|
@@ -136,6 +149,7 @@ export class MCPClient {
|
|
|
136
149
|
this.status = 'disconnected'
|
|
137
150
|
this.connectedAt = undefined
|
|
138
151
|
this.log.info('MCP client disconnected')
|
|
152
|
+
this.emitLifecycle({ type: 'mcp_client_disconnected', clientId: this.id })
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
isConnected(): boolean {
|
|
@@ -181,6 +195,48 @@ export class MCPClient {
|
|
|
181
195
|
return result.contents
|
|
182
196
|
}
|
|
183
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The prompts a server publishes.
|
|
200
|
+
*
|
|
201
|
+
* A prompt is the server's own wording for a task it knows how to set
|
|
202
|
+
* up — the half of MCP that is not tools. `MCPPromptDefinition` and
|
|
203
|
+
* `MCPPromptArgument` were declared when the types were written and no
|
|
204
|
+
* method ever asked for one, so a server offering prompts had them
|
|
205
|
+
* silently ignored.
|
|
206
|
+
*
|
|
207
|
+
* Paged through the same reader as every other list, which is the point
|
|
208
|
+
* of it being generic: a server that pages its prompts does not get
|
|
209
|
+
* silently truncated to page one the way the tool list once was.
|
|
210
|
+
*/
|
|
211
|
+
async listPrompts(): Promise<MCPPromptDefinition[]> {
|
|
212
|
+
this.requireConnected()
|
|
213
|
+
return await this.listAllPages('prompts/list', 'prompts')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fetch one prompt, with its arguments filled in.
|
|
218
|
+
*
|
|
219
|
+
* Returns the messages the SERVER composed. They are data to be shown to
|
|
220
|
+
* a model, never instructions to this client: a prompt arriving from a
|
|
221
|
+
* remote server is exactly the untrusted-content case, and treating its
|
|
222
|
+
* text as direction would let a server steer the agent by publishing a
|
|
223
|
+
* prompt nobody asked to run.
|
|
224
|
+
*/
|
|
225
|
+
async getPrompt(
|
|
226
|
+
name: string,
|
|
227
|
+
args?: Record<string, string>,
|
|
228
|
+
): Promise<{ description?: string; messages: MCPPromptMessage[] }> {
|
|
229
|
+
this.requireConnected()
|
|
230
|
+
const result = (await this.request('prompts/get', {
|
|
231
|
+
name,
|
|
232
|
+
arguments: args ?? {},
|
|
233
|
+
})) as { description?: string; messages?: MCPPromptMessage[] }
|
|
234
|
+
return {
|
|
235
|
+
...(result.description !== undefined ? { description: result.description } : {}),
|
|
236
|
+
messages: result.messages ?? [],
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
184
240
|
async listResourceTemplates(): Promise<MCPResourceTemplate[]> {
|
|
185
241
|
this.requireConnected()
|
|
186
242
|
return await this.listAllPages('resources/templates/list', 'resourceTemplates')
|
|
@@ -231,6 +287,45 @@ export class MCPClient {
|
|
|
231
287
|
this.notificationHandlers.push(handler)
|
|
232
288
|
}
|
|
233
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Watch this client come up, go down, or fail.
|
|
292
|
+
*
|
|
293
|
+
* `MCPLifecycleEvent` and `MCPEventListener` were declared with the rest
|
|
294
|
+
* of the MCP types and nothing ever emitted one, so a host could observe
|
|
295
|
+
* a server dying only by noticing that calls had started failing. The
|
|
296
|
+
* four transitions below already existed and already mutated `status`;
|
|
297
|
+
* this adds no state, it just says out loud what the client already
|
|
298
|
+
* knew.
|
|
299
|
+
*
|
|
300
|
+
* Returns an unsubscribe. `onNotification` above does not, which is the
|
|
301
|
+
* bug this avoids repeating: a listener that cannot be removed keeps a
|
|
302
|
+
* disposed host object alive for as long as the client lives.
|
|
303
|
+
*/
|
|
304
|
+
onLifecycle(listener: MCPEventListener): () => void {
|
|
305
|
+
this.lifecycleListeners.push(listener)
|
|
306
|
+
return () => {
|
|
307
|
+
const index = this.lifecycleListeners.indexOf(listener)
|
|
308
|
+
if (index >= 0) this.lifecycleListeners.splice(index, 1)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A listener that throws must not take the transport down with it.
|
|
314
|
+
*
|
|
315
|
+
* These fire from inside transport callbacks and from the failure path
|
|
316
|
+
* of `connect`, so an exception here would surface as a connection
|
|
317
|
+
* error — blaming the server for a bug in the host's own observer.
|
|
318
|
+
*/
|
|
319
|
+
private emitLifecycle(event: MCPLifecycleEvent): void {
|
|
320
|
+
for (const listener of this.lifecycleListeners) {
|
|
321
|
+
try {
|
|
322
|
+
listener(event)
|
|
323
|
+
} catch (err) {
|
|
324
|
+
this.log.warn('MCP lifecycle listener threw', { error: toErrorMessage(err) })
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
234
329
|
private createTransport(config: MCPTransportUnion): MCPTransport {
|
|
235
330
|
switch (config.type) {
|
|
236
331
|
case 'stdio':
|
|
@@ -38,7 +38,18 @@ export class MCPToolDiscovery {
|
|
|
38
38
|
private clients: MCPClient[]
|
|
39
39
|
private log: Logger
|
|
40
40
|
private options: MCPToolDiscoveryOptions
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Last admitted tool set per SERVER, for drift detection.
|
|
43
|
+
*
|
|
44
|
+
* Keyed by server name rather than client id, and the difference is the
|
|
45
|
+
* whole point. A client id is minted per connection, so on the path a
|
|
46
|
+
* real MCP server actually takes — a plugin enabling, connecting, and
|
|
47
|
+
* being disabled again — every discovery was the first one that id had
|
|
48
|
+
* ever seen, and drift could not fire however many times the server
|
|
49
|
+
* changed underneath. The threat is a server that advertises something
|
|
50
|
+
* benign when a host approves it and something else afterwards, which is
|
|
51
|
+
* a property of the SERVER across connections.
|
|
52
|
+
*/
|
|
42
53
|
private lastSeen = new Map<string, MCPToolDefinition[]>()
|
|
43
54
|
|
|
44
55
|
constructor(clients: MCPClient[], options: MCPToolDiscoveryOptions = {}) {
|
|
@@ -53,7 +64,11 @@ export class MCPToolDiscovery {
|
|
|
53
64
|
|
|
54
65
|
removeClient(clientId: string): void {
|
|
55
66
|
this.clients = this.clients.filter((c) => c.id !== clientId)
|
|
56
|
-
|
|
67
|
+
// The remembered tool set is deliberately NOT forgotten. Dropping it on
|
|
68
|
+
// disconnect is what made the rug pull invisible: disable, swap the
|
|
69
|
+
// server's tools, enable again, and the next discovery had nothing to
|
|
70
|
+
// compare against. What is remembered is a name and a list of tool
|
|
71
|
+
// shapes, so keeping it costs almost nothing.
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
async discoverAll(): Promise<MCPDiscoveredTool[]> {
|
|
@@ -119,8 +134,8 @@ export class MCPToolDiscovery {
|
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
private detectDrift(clientId: string, serverName: string, admitted: MCPToolDefinition[]): void {
|
|
122
|
-
const previous = this.lastSeen.get(
|
|
123
|
-
this.lastSeen.set(
|
|
137
|
+
const previous = this.lastSeen.get(serverName)
|
|
138
|
+
this.lastSeen.set(serverName, admitted)
|
|
124
139
|
if (!previous) return
|
|
125
140
|
|
|
126
141
|
const drift = diffTools(previous, admitted)
|
|
@@ -17,8 +17,12 @@ export { MCPToolDiscovery } from './discovery.js'
|
|
|
17
17
|
|
|
18
18
|
export { MCPConnectorBridge } from '../../bridge/mcp/connector/adapter.js'
|
|
19
19
|
|
|
20
|
-
export { MCPServer } from './server.js'
|
|
21
|
-
export type {
|
|
20
|
+
export { MCPServer, MCPMethodNotFound } from './server.js'
|
|
21
|
+
export type {
|
|
22
|
+
MCPServerToolProvider,
|
|
23
|
+
MCPServerResourceProvider,
|
|
24
|
+
MCPServerPromptProvider,
|
|
25
|
+
} from './server.js'
|
|
22
26
|
|
|
23
27
|
export type { MCPToolDiscoveryOptions } from './discovery.js'
|
|
24
28
|
export { applyToolPolicy, diffTools, hasDrift, toolsHash } from './policy.js'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JSON_RPC_METHOD_NOT_FOUND } from '../../constants/mcp/index.js'
|
|
1
2
|
import type {
|
|
2
3
|
MCPContentBlock,
|
|
3
4
|
MCPJsonRpcMessage,
|
|
@@ -9,11 +10,27 @@ import type {
|
|
|
9
10
|
MCPToolResult,
|
|
10
11
|
MCPTransport,
|
|
11
12
|
} from '../../types/connector/index.js'
|
|
13
|
+
import type { MCPPromptDefinition, MCPPromptMessage } from '../../types/connector/index.js'
|
|
12
14
|
import type { MCPServerId } from '../../types/ids/index.js'
|
|
13
15
|
import { toErrorMessage } from '../../utils/error.js'
|
|
14
16
|
import { generateMCPServerId } from '../../utils/id.js'
|
|
15
17
|
import { type Logger, getRootLogger } from '../../utils/logger.js'
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A method this server does not implement.
|
|
21
|
+
*
|
|
22
|
+
* Carried as its own error so the dispatcher can answer with the protocol's
|
|
23
|
+
* own code instead of a generic internal error. `-32603` tells a client the
|
|
24
|
+
* server broke; `-32601` tells it the server does not do this — and only the
|
|
25
|
+
* second lets a client stop asking rather than retry.
|
|
26
|
+
*/
|
|
27
|
+
export class MCPMethodNotFound extends Error {
|
|
28
|
+
constructor(readonly method: string) {
|
|
29
|
+
super(`Method not found: ${method}`)
|
|
30
|
+
this.name = 'MCPMethodNotFound'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
export interface MCPServerToolProvider {
|
|
18
35
|
listTools(): MCPToolDefinition[]
|
|
19
36
|
callTool(name: string, args?: Record<string, unknown>): Promise<MCPToolResult>
|
|
@@ -24,11 +41,27 @@ export interface MCPServerResourceProvider {
|
|
|
24
41
|
readResource(uri: string): Promise<MCPContentBlock[]>
|
|
25
42
|
}
|
|
26
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Prompts this server publishes.
|
|
46
|
+
*
|
|
47
|
+
* Injected as an interface like the other two, so the server owns dispatch
|
|
48
|
+
* and the host owns content. `MCPPromptDefinition` existed in the types
|
|
49
|
+
* from the start with no way to serve one.
|
|
50
|
+
*/
|
|
51
|
+
export interface MCPServerPromptProvider {
|
|
52
|
+
listPrompts(): MCPPromptDefinition[]
|
|
53
|
+
getPrompt(
|
|
54
|
+
name: string,
|
|
55
|
+
args?: Record<string, string>,
|
|
56
|
+
): Promise<{ description?: string; messages: MCPPromptMessage[] }>
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
export class MCPServer {
|
|
28
60
|
readonly id: MCPServerId
|
|
29
61
|
private config: MCPServerConfig
|
|
30
62
|
private toolProvider: MCPServerToolProvider
|
|
31
63
|
private resourceProvider?: MCPServerResourceProvider
|
|
64
|
+
private promptProvider?: MCPServerPromptProvider
|
|
32
65
|
private transport: MCPTransport | null = null
|
|
33
66
|
private running = false
|
|
34
67
|
private connectedClients = 0
|
|
@@ -39,11 +72,13 @@ export class MCPServer {
|
|
|
39
72
|
config: MCPServerConfig,
|
|
40
73
|
toolProvider: MCPServerToolProvider,
|
|
41
74
|
resourceProvider?: MCPServerResourceProvider,
|
|
75
|
+
promptProvider?: MCPServerPromptProvider,
|
|
42
76
|
) {
|
|
43
77
|
this.id = config.id ?? generateMCPServerId()
|
|
44
78
|
this.config = config
|
|
45
79
|
this.toolProvider = toolProvider
|
|
46
80
|
this.resourceProvider = resourceProvider
|
|
81
|
+
this.promptProvider = promptProvider
|
|
47
82
|
this.log = getRootLogger().child({ component: 'MCPServer', serverId: this.id })
|
|
48
83
|
}
|
|
49
84
|
|
|
@@ -100,7 +135,8 @@ export class MCPServer {
|
|
|
100
135
|
const result = await this.dispatch(message.method, message.params ?? {})
|
|
101
136
|
await this.respond(message.id, result)
|
|
102
137
|
} catch (err) {
|
|
103
|
-
|
|
138
|
+
const code = err instanceof MCPMethodNotFound ? JSON_RPC_METHOD_NOT_FOUND : -32603
|
|
139
|
+
await this.respondError(message.id, code, toErrorMessage(err))
|
|
104
140
|
}
|
|
105
141
|
}
|
|
106
142
|
|
|
@@ -116,10 +152,14 @@ export class MCPServer {
|
|
|
116
152
|
return this.handleResourcesList()
|
|
117
153
|
case 'resources/read':
|
|
118
154
|
return this.handleResourcesRead(params)
|
|
155
|
+
case 'prompts/list':
|
|
156
|
+
return this.handlePromptsList()
|
|
157
|
+
case 'prompts/get':
|
|
158
|
+
return this.handlePromptsGet(params)
|
|
119
159
|
case 'ping':
|
|
120
160
|
return {}
|
|
121
161
|
default:
|
|
122
|
-
throw new
|
|
162
|
+
throw new MCPMethodNotFound(method)
|
|
123
163
|
}
|
|
124
164
|
}
|
|
125
165
|
|
|
@@ -138,6 +178,13 @@ export class MCPServer {
|
|
|
138
178
|
capabilities.resources = { subscribe: false, listChanged: false }
|
|
139
179
|
}
|
|
140
180
|
|
|
181
|
+
// Declared only when there is something behind it, matching resources
|
|
182
|
+
// above. A capability advertised without a provider is a promise the
|
|
183
|
+
// next call breaks.
|
|
184
|
+
if (this.promptProvider) {
|
|
185
|
+
capabilities.prompts = { listChanged: false }
|
|
186
|
+
}
|
|
187
|
+
|
|
141
188
|
return {
|
|
142
189
|
protocolVersion: '2024-11-05',
|
|
143
190
|
capabilities,
|
|
@@ -166,13 +213,64 @@ export class MCPServer {
|
|
|
166
213
|
return this.toolProvider.callTool(name, args)
|
|
167
214
|
}
|
|
168
215
|
|
|
216
|
+
/**
|
|
217
|
+
* An empty list is what a server with NO resources returns. A server
|
|
218
|
+
* that cannot serve resources at all has to say so differently.
|
|
219
|
+
*
|
|
220
|
+
* This used to answer `{ resources: [] }` when no provider was
|
|
221
|
+
* configured, for a capability `initialize` never advertised — so a
|
|
222
|
+
* client that asked anyway was told, in the protocol's own vocabulary,
|
|
223
|
+
* that the answer is "none" rather than "not here". The two send a
|
|
224
|
+
* client in opposite directions: one stops asking, the other looks for
|
|
225
|
+
* the resource somewhere else.
|
|
226
|
+
*/
|
|
169
227
|
private handleResourcesList(): { resources: MCPResource[] } {
|
|
170
228
|
if (!this.resourceProvider) {
|
|
171
|
-
|
|
229
|
+
throw new MCPMethodNotFound('resources/list')
|
|
172
230
|
}
|
|
173
231
|
return { resources: this.resourceProvider.listResources() }
|
|
174
232
|
}
|
|
175
233
|
|
|
234
|
+
private handlePromptsList(): { prompts: MCPPromptDefinition[] } {
|
|
235
|
+
if (!this.promptProvider) {
|
|
236
|
+
throw new MCPMethodNotFound('prompts/list')
|
|
237
|
+
}
|
|
238
|
+
return { prompts: this.promptProvider.listPrompts() }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async handlePromptsGet(
|
|
242
|
+
params: Record<string, unknown>,
|
|
243
|
+
): Promise<{ description?: string; messages: MCPPromptMessage[] }> {
|
|
244
|
+
if (!this.promptProvider) {
|
|
245
|
+
throw new MCPMethodNotFound('prompts/get')
|
|
246
|
+
}
|
|
247
|
+
const name = params.name as string
|
|
248
|
+
if (!name) {
|
|
249
|
+
throw new Error('prompts/get requires a "name"')
|
|
250
|
+
}
|
|
251
|
+
const args = (params.arguments as Record<string, string> | undefined) ?? {}
|
|
252
|
+
|
|
253
|
+
// Required arguments are checked HERE rather than left to the
|
|
254
|
+
// provider. A prompt declares them, so a missing one is answerable
|
|
255
|
+
// from the declaration alone, and every provider would otherwise
|
|
256
|
+
// re-implement the same check or forget to.
|
|
257
|
+
const declared = this.promptProvider.listPrompts().find((p) => p.name === name)
|
|
258
|
+
if (!declared) {
|
|
259
|
+
throw new Error(`Unknown prompt: ${name}`)
|
|
260
|
+
}
|
|
261
|
+
const missing = (declared.arguments ?? [])
|
|
262
|
+
.filter((a) => a.required === true)
|
|
263
|
+
.map((a) => a.name)
|
|
264
|
+
.filter((n) => args[n] === undefined)
|
|
265
|
+
if (missing.length > 0) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`prompts/get "${name}" is missing required argument(s): ${missing.join(', ')}`,
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return this.promptProvider.getPrompt(name, args)
|
|
272
|
+
}
|
|
273
|
+
|
|
176
274
|
private async handleResourcesRead(
|
|
177
275
|
params: Record<string, unknown>,
|
|
178
276
|
): Promise<{ contents: MCPContentBlock[] }> {
|