@namzu/sdk 3.1.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.
Files changed (257) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
  3. package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
  4. package/dist/advisory/__tests__/consultation-context.test.js +124 -0
  5. package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
  6. package/dist/advisory/context.d.ts +25 -0
  7. package/dist/advisory/context.d.ts.map +1 -1
  8. package/dist/advisory/context.js +18 -0
  9. package/dist/advisory/context.js.map +1 -1
  10. package/dist/advisory/executor.d.ts.map +1 -1
  11. package/dist/advisory/executor.js +25 -3
  12. package/dist/advisory/executor.js.map +1 -1
  13. package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
  14. package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
  15. package/dist/compaction/__tests__/context-reducer.test.js +197 -0
  16. package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
  17. package/dist/compaction/factory.d.ts +7 -0
  18. package/dist/compaction/factory.d.ts.map +1 -1
  19. package/dist/compaction/factory.js +7 -0
  20. package/dist/compaction/factory.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -0
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js +1 -0
  24. package/dist/compaction/index.js.map +1 -1
  25. package/dist/compaction/interface.d.ts +13 -0
  26. package/dist/compaction/interface.d.ts.map +1 -1
  27. package/dist/compaction/managers/null.d.ts +3 -0
  28. package/dist/compaction/managers/null.d.ts.map +1 -1
  29. package/dist/compaction/managers/null.js +3 -0
  30. package/dist/compaction/managers/null.js.map +1 -1
  31. package/dist/compaction/managers/slidingWindow.d.ts +6 -0
  32. package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
  33. package/dist/compaction/managers/slidingWindow.js +6 -0
  34. package/dist/compaction/managers/slidingWindow.js.map +1 -1
  35. package/dist/compaction/managers/structured.d.ts +10 -0
  36. package/dist/compaction/managers/structured.d.ts.map +1 -1
  37. package/dist/compaction/managers/structured.js +10 -0
  38. package/dist/compaction/managers/structured.js.map +1 -1
  39. package/dist/compaction/reducer.d.ts +86 -0
  40. package/dist/compaction/reducer.d.ts.map +1 -0
  41. package/dist/compaction/reducer.js +77 -0
  42. package/dist/compaction/reducer.js.map +1 -0
  43. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
  44. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
  45. package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
  46. package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
  47. package/dist/connector/builtins/http.d.ts.map +1 -1
  48. package/dist/connector/builtins/http.js +24 -2
  49. package/dist/connector/builtins/http.js.map +1 -1
  50. package/dist/connector/builtins/http.test.js +18 -2
  51. package/dist/connector/builtins/http.test.js.map +1 -1
  52. package/dist/connector/index.d.ts +2 -2
  53. package/dist/connector/index.d.ts.map +1 -1
  54. package/dist/connector/index.js +1 -1
  55. package/dist/connector/index.js.map +1 -1
  56. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
  57. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
  58. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
  59. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
  60. package/dist/connector/mcp/client.d.ts +52 -1
  61. package/dist/connector/mcp/client.d.ts.map +1 -1
  62. package/dist/connector/mcp/client.js +86 -0
  63. package/dist/connector/mcp/client.js.map +1 -1
  64. package/dist/connector/mcp/discovery.d.ts +12 -1
  65. package/dist/connector/mcp/discovery.d.ts.map +1 -1
  66. package/dist/connector/mcp/discovery.js +19 -4
  67. package/dist/connector/mcp/discovery.js.map +1 -1
  68. package/dist/connector/mcp/index.d.ts +2 -2
  69. package/dist/connector/mcp/index.d.ts.map +1 -1
  70. package/dist/connector/mcp/index.js +1 -1
  71. package/dist/connector/mcp/index.js.map +1 -1
  72. package/dist/connector/mcp/server.d.ts +42 -1
  73. package/dist/connector/mcp/server.d.ts.map +1 -1
  74. package/dist/connector/mcp/server.js +77 -4
  75. package/dist/connector/mcp/server.js.map +1 -1
  76. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
  77. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
  78. package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
  79. package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
  80. package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
  81. package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
  82. package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
  83. package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
  84. package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
  85. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
  86. package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
  87. package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
  88. package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
  89. package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
  90. package/dist/plugin/lifecycle.d.ts +41 -0
  91. package/dist/plugin/lifecycle.d.ts.map +1 -1
  92. package/dist/plugin/lifecycle.js +29 -1
  93. package/dist/plugin/lifecycle.js.map +1 -1
  94. package/dist/plugin/loader.d.ts +39 -3
  95. package/dist/plugin/loader.d.ts.map +1 -1
  96. package/dist/plugin/loader.js +37 -4
  97. package/dist/plugin/loader.js.map +1 -1
  98. package/dist/public-runtime.d.ts +4 -2
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +5 -2
  101. package/dist/public-runtime.js.map +1 -1
  102. package/dist/public-types.d.ts +2 -2
  103. package/dist/public-types.d.ts.map +1 -1
  104. package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
  105. package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
  106. package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
  107. package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
  108. package/dist/rag/ingestion.d.ts.map +1 -1
  109. package/dist/rag/ingestion.js +1 -0
  110. package/dist/rag/ingestion.js.map +1 -1
  111. package/dist/rag/retriever.d.ts.map +1 -1
  112. package/dist/rag/retriever.js +2 -0
  113. package/dist/rag/retriever.js.map +1 -1
  114. package/dist/rag/vector-store.d.ts.map +1 -1
  115. package/dist/rag/vector-store.js +6 -0
  116. package/dist/rag/vector-store.js.map +1 -1
  117. package/dist/registry/tool/execute.d.ts.map +1 -1
  118. package/dist/registry/tool/execute.js +113 -109
  119. package/dist/registry/tool/execute.js.map +1 -1
  120. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  121. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  122. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  125. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  128. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  129. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  132. package/dist/runtime/query/index.d.ts +10 -0
  133. package/dist/runtime/query/index.d.ts.map +1 -1
  134. package/dist/runtime/query/index.js +26 -0
  135. package/dist/runtime/query/index.js.map +1 -1
  136. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  137. package/dist/runtime/query/iteration/index.js +96 -34
  138. package/dist/runtime/query/iteration/index.js.map +1 -1
  139. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  140. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  141. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  142. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  143. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  144. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  145. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  147. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  148. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  149. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  150. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  151. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  153. package/dist/runtime/query/resume-run.d.ts +70 -0
  154. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  155. package/dist/runtime/query/resume-run.js +46 -0
  156. package/dist/runtime/query/resume-run.js.map +1 -0
  157. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  158. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  159. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  160. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  161. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  162. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  163. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  164. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  165. package/dist/tools/advisory/index.js +1 -1
  166. package/dist/tools/advisory/index.js.map +1 -1
  167. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  168. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  169. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  170. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  171. package/dist/tools/coordinator/index.d.ts.map +1 -1
  172. package/dist/tools/coordinator/index.js +13 -1
  173. package/dist/tools/coordinator/index.js.map +1 -1
  174. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  175. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  176. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  177. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  178. package/dist/types/agent/supervisor.d.ts +15 -0
  179. package/dist/types/agent/supervisor.d.ts.map +1 -1
  180. package/dist/types/connector/core.d.ts +34 -0
  181. package/dist/types/connector/core.d.ts.map +1 -1
  182. package/dist/types/connector/definition.d.ts +10 -0
  183. package/dist/types/connector/definition.d.ts.map +1 -1
  184. package/dist/types/connector/mcp.d.ts +13 -0
  185. package/dist/types/connector/mcp.d.ts.map +1 -1
  186. package/dist/types/rag/retrieval.d.ts +16 -0
  187. package/dist/types/rag/retrieval.d.ts.map +1 -1
  188. package/dist/types/rag/storage.d.ts +9 -0
  189. package/dist/types/rag/storage.d.ts.map +1 -1
  190. package/dist/types/rag/vector.d.ts +11 -0
  191. package/dist/types/rag/vector.d.ts.map +1 -1
  192. package/dist/types/router/task-router.d.ts +19 -0
  193. package/dist/types/router/task-router.d.ts.map +1 -1
  194. package/dist/types/run/prepare-step.d.ts +56 -3
  195. package/dist/types/run/prepare-step.d.ts.map +1 -1
  196. package/dist/types/toolset/index.d.ts +22 -0
  197. package/dist/types/toolset/index.d.ts.map +1 -1
  198. package/package.json +1 -1
  199. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  200. package/src/advisory/context.ts +32 -0
  201. package/src/advisory/executor.ts +30 -3
  202. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  203. package/src/compaction/factory.ts +7 -0
  204. package/src/compaction/index.ts +8 -0
  205. package/src/compaction/interface.ts +13 -0
  206. package/src/compaction/managers/null.ts +3 -0
  207. package/src/compaction/managers/slidingWindow.ts +6 -0
  208. package/src/compaction/managers/structured.ts +10 -0
  209. package/src/compaction/reducer.ts +154 -0
  210. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  211. package/src/connector/builtins/http.test.ts +28 -2
  212. package/src/connector/builtins/http.ts +26 -2
  213. package/src/connector/index.ts +6 -2
  214. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  215. package/src/connector/mcp/client.ts +95 -0
  216. package/src/connector/mcp/discovery.ts +19 -4
  217. package/src/connector/mcp/index.ts +6 -2
  218. package/src/connector/mcp/server.ts +101 -3
  219. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  220. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  221. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  222. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  223. package/src/plugin/lifecycle.ts +57 -1
  224. package/src/plugin/loader.ts +57 -3
  225. package/src/public-runtime.ts +8 -0
  226. package/src/public-types.ts +5 -0
  227. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  228. package/src/rag/ingestion.ts +1 -0
  229. package/src/rag/retriever.ts +2 -0
  230. package/src/rag/vector-store.ts +5 -0
  231. package/src/registry/tool/execute.ts +123 -119
  232. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  233. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  234. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  235. package/src/runtime/query/index.ts +39 -0
  236. package/src/runtime/query/iteration/index.ts +103 -34
  237. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  238. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  239. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  240. package/src/runtime/query/iteration/phases/context.ts +11 -0
  241. package/src/runtime/query/resume-run.ts +93 -0
  242. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  243. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  244. package/src/tools/advisory/index.ts +1 -1
  245. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  246. package/src/tools/coordinator/index.ts +14 -1
  247. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  248. package/src/types/agent/supervisor.ts +15 -0
  249. package/src/types/connector/core.ts +34 -0
  250. package/src/types/connector/definition.ts +10 -0
  251. package/src/types/connector/mcp.ts +14 -0
  252. package/src/types/rag/retrieval.ts +16 -0
  253. package/src/types/rag/storage.ts +9 -0
  254. package/src/types/rag/vector.ts +11 -0
  255. package/src/types/router/task-router.ts +19 -0
  256. package/src/types/run/prepare-step.ts +58 -3
  257. package/src/types/toolset/index.ts +22 -0
@@ -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
- /** Last admitted tool set per client, for drift detection. */
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
- this.lastSeen.delete(clientId)
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(clientId)
123
- this.lastSeen.set(clientId, admitted)
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 { MCPServerToolProvider, MCPServerResourceProvider } from './server.js'
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
- await this.respondError(message.id, -32603, toErrorMessage(err))
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 Error(`Unknown method: ${method}`)
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
- return { resources: [] }
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[] }> {
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { AgentTaskContext } from '../../../types/agent/index.js'
4
+
5
+ /**
6
+ * Which `maxDepth` is in force.
7
+ *
8
+ * Two exist. `AgentManagerConfig.maxDepth` is enforced in
9
+ * `AgentManager.sendMessage`; `SupervisorAgentConfig.maxDepth` is read by
10
+ * nothing. For a recursion bound that is the worst way to be wrong — the
11
+ * number a reviewer sees in the supervisor config is not the number
12
+ * stopping runaway delegation.
13
+ *
14
+ * The first version of these tests passed the config where the registry
15
+ * goes, so every case silently ran against the default of 3 — and the case
16
+ * that used 3 "passed" while proving nothing. Configuring a value the
17
+ * default is not is what makes this a test.
18
+ */
19
+
20
+ async function manager(maxDepth: number) {
21
+ const { AgentManager } = await import('../lifecycle.js')
22
+ const registry = { resolve: vi.fn(), get: vi.fn(), getAll: vi.fn(() => []) }
23
+ const deps = { createAgent: vi.fn(), taskStore: undefined }
24
+ return new AgentManager(registry as never, { maxDepth }, deps as never)
25
+ }
26
+
27
+ function contextAtDepth(depth: number): AgentTaskContext {
28
+ return {
29
+ parentRunId: 'run_x',
30
+ parentAgentId: 'agent_x',
31
+ parentAbortController: new AbortController(),
32
+ depth,
33
+ tenantId: 'tnt_x',
34
+ } as unknown as AgentTaskContext
35
+ }
36
+
37
+ function send(m: Awaited<ReturnType<typeof manager>>, depth: number) {
38
+ return m.sendMessage(
39
+ { agentId: 'a', prompt: 'go', tenantId: 'tnt_x' } as never,
40
+ contextAtDepth(depth),
41
+ )
42
+ }
43
+
44
+ describe('the manager owns the recursion bound', () => {
45
+ it('refuses a task at the configured depth', async () => {
46
+ // 5, deliberately not the default of 3: a limit that happens to equal
47
+ // the default cannot tell "configured" from "ignored".
48
+ const mgr = await manager(5)
49
+
50
+ await expect(send(mgr, 5)).rejects.toThrow(/Max task depth 5/)
51
+ })
52
+
53
+ it('refuses past it too, not only exactly at it', async () => {
54
+ const mgr = await manager(2)
55
+
56
+ await expect(send(mgr, 7)).rejects.toThrow(/Max task depth 2/)
57
+ })
58
+
59
+ it('reports the depth reached alongside the limit', async () => {
60
+ const mgr = await manager(2)
61
+
62
+ // A limit without the current value sends someone to instrument the
63
+ // run to learn how deep it actually got.
64
+ await expect(send(mgr, 4)).rejects.toThrow(/current: 4/)
65
+ })
66
+
67
+ it('allows a task below the bound to proceed past this check', async () => {
68
+ const mgr = await manager(5)
69
+
70
+ // It gets past the depth gate and fails later on the stubbed
71
+ // registry — which is the point: the gate did not stop it.
72
+ await expect(send(mgr, 1)).rejects.not.toThrow(/Max task depth/)
73
+ })
74
+ })
@@ -0,0 +1,133 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { PluginRuntimeConfigSchema } from '../../config/runtime.js'
7
+ import {
8
+ PLUGIN_MANIFEST_FILENAME,
9
+ PROJECT_PLUGIN_DIR,
10
+ USER_PLUGIN_DIR,
11
+ } from '../../constants/plugin/index.js'
12
+
13
+ /**
14
+ * `allowedScopes` read exactly like a trust boundary and enforced nothing.
15
+ *
16
+ * A plugin is arbitrary code with hooks into tool execution, and the two
17
+ * scopes are not equally trusted: a project plugin is reviewable in the repo
18
+ * the agent works on, a user plugin comes from a home directory the repo's
19
+ * reviewers never see. `PluginRuntimeConfig` carried `enabled`,
20
+ * `autoDiscovery` and `allowedScopes`; nothing anywhere consulted any of the
21
+ * three, and discovery scanned both locations unconditionally.
22
+ */
23
+
24
+ let home: string
25
+ let workdir: string
26
+
27
+ vi.mock('node:os', async () => {
28
+ const actual = await vi.importActual<typeof import('node:os')>('node:os')
29
+ return { ...actual, homedir: () => home }
30
+ })
31
+
32
+ async function plantPlugin(dir: string, name: string): Promise<void> {
33
+ const pluginDir = join(dir, name)
34
+ await mkdir(pluginDir, { recursive: true })
35
+ await writeFile(
36
+ join(pluginDir, PLUGIN_MANIFEST_FILENAME),
37
+ JSON.stringify({ name, version: '1.0.0' }),
38
+ 'utf-8',
39
+ )
40
+ }
41
+
42
+ beforeEach(async () => {
43
+ home = await mkdtemp(join(tmpdir(), 'namzu-home-'))
44
+ workdir = await mkdtemp(join(tmpdir(), 'namzu-work-'))
45
+ await plantPlugin(join(workdir, PROJECT_PLUGIN_DIR), 'project-plugin')
46
+ await plantPlugin(join(home, USER_PLUGIN_DIR), 'user-plugin')
47
+ })
48
+
49
+ afterEach(async () => {
50
+ await Promise.all([
51
+ rm(home, { recursive: true, force: true }),
52
+ rm(workdir, { recursive: true, force: true }),
53
+ ])
54
+ })
55
+
56
+ async function discover(
57
+ options?: Parameters<typeof import('../loader.js').discoverAllPluginDirs>[1],
58
+ ) {
59
+ const { discoverAllPluginDirs } = await import('../loader.js')
60
+ return discoverAllPluginDirs(workdir, options)
61
+ }
62
+
63
+ describe('a scope the host did not allow is not scanned', () => {
64
+ it('finds both when nothing is said', async () => {
65
+ const found = await discover()
66
+
67
+ expect(found.project).toHaveLength(1)
68
+ expect(found.user).toHaveLength(1)
69
+ })
70
+
71
+ it('keeps user plugins out when only project is allowed', async () => {
72
+ const found = await discover({ allowedScopes: ['project'] })
73
+
74
+ expect(found.project).toHaveLength(1)
75
+ // The setting that reads like a boundary now is one.
76
+ expect(found.user).toEqual([])
77
+ })
78
+
79
+ it('keeps project plugins out when only user is allowed', async () => {
80
+ const found = await discover({ allowedScopes: ['user'] })
81
+
82
+ expect(found.project).toEqual([])
83
+ expect(found.user).toHaveLength(1)
84
+ })
85
+
86
+ it('finds nothing when neither scope is allowed', async () => {
87
+ const found = await discover({ allowedScopes: [] })
88
+
89
+ expect(found).toEqual({ project: [], user: [] })
90
+ })
91
+ })
92
+
93
+ describe('the runtime switches turn discovery off', () => {
94
+ it('discovers nothing when the plugin runtime is disabled', async () => {
95
+ const found = await discover({ enabled: false })
96
+
97
+ expect(found).toEqual({ project: [], user: [] })
98
+ })
99
+
100
+ it('discovers nothing when auto-discovery is off', async () => {
101
+ const found = await discover({ autoDiscovery: false })
102
+
103
+ expect(found).toEqual({ project: [], user: [] })
104
+ })
105
+
106
+ it('a default-parsed config discovers nothing, because the runtime ships off', async () => {
107
+ // `enabled` defaults to false in the schema. A host who parses the
108
+ // config and passes it gets the documented default rather than a
109
+ // silently-on plugin runtime.
110
+ const found = await discover(PluginRuntimeConfigSchema.parse({}))
111
+
112
+ expect(found).toEqual({ project: [], user: [] })
113
+ })
114
+
115
+ it('an enabled default-parsed config finds both scopes', async () => {
116
+ const found = await discover(PluginRuntimeConfigSchema.parse({ enabled: true }))
117
+
118
+ expect(found.project).toHaveLength(1)
119
+ expect(found.user).toHaveLength(1)
120
+ })
121
+
122
+ it('a parsed config carries its scope restriction through', async () => {
123
+ const config = PluginRuntimeConfigSchema.parse({
124
+ enabled: true,
125
+ allowedScopes: ['project'],
126
+ })
127
+
128
+ const found = await discover(config)
129
+
130
+ expect(found.project).toHaveLength(1)
131
+ expect(found.user).toEqual([])
132
+ })
133
+ })
@@ -11,11 +11,15 @@ const mockDisconnect = vi.fn(async (): Promise<void> => undefined)
11
11
  const mockListTools = vi.fn(async (): Promise<unknown[]> => [])
12
12
 
13
13
  vi.mock('../../connector/mcp/client.js', () => ({
14
- MCPClient: vi.fn().mockImplementation(() => ({
14
+ MCPClient: vi.fn().mockImplementation((config: { serverName: string }) => ({
15
15
  id: 'mcp-client-mock',
16
16
  connect: mockConnect,
17
17
  disconnect: mockDisconnect,
18
18
  listTools: mockListTools,
19
+ // The real client has always had this; the mock did not, which went
20
+ // unnoticed while nothing on this path asked the client which server
21
+ // it was talking to. Admission does — a policy is per server name.
22
+ getState: () => ({ serverName: config.serverName }),
19
23
  })),
20
24
  }))
21
25