@jini-ai/mcp 0.3.4 → 0.4.1
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 +156 -0
- package/README.md +97 -22
- package/dist/agent-install/install.d.ts +20 -4
- package/dist/agent-install/install.js +11 -11
- package/dist/bin/serve.d.ts +25 -13
- package/dist/bin/serve.js +42 -21
- package/dist/client/client.d.ts +28 -8
- package/dist/client/client.js +25 -21
- package/dist/core/config.d.ts +57 -18
- package/dist/core/config.js +76 -48
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.js +1 -2
- package/dist/core/install-info.d.ts +4 -1
- package/dist/core/install-info.js +5 -5
- package/dist/core/tokens.d.ts +39 -13
- package/dist/core/tokens.js +54 -24
- package/dist/federation/adapter.http.d.ts +67 -0
- package/dist/federation/adapter.http.js +414 -0
- package/dist/federation/approvals/call-confirmation.d.ts +23 -0
- package/dist/federation/approvals/call-confirmation.js +124 -0
- package/dist/federation/approvals/index.d.ts +4 -0
- package/dist/federation/approvals/index.js +4 -0
- package/dist/federation/approvals/ports.d.ts +107 -0
- package/dist/federation/approvals/ports.js +2 -0
- package/dist/federation/approvals/revocation.d.ts +61 -0
- package/dist/federation/approvals/revocation.js +52 -0
- package/dist/federation/bootstrap.d.ts +100 -0
- package/dist/federation/bootstrap.js +182 -0
- package/dist/federation/config.d.ts +68 -0
- package/dist/federation/config.js +40 -0
- package/dist/federation/index.d.ts +14 -0
- package/dist/federation/index.js +14 -0
- package/dist/federation/mcp-protocol.d.ts +115 -0
- package/dist/federation/mcp-protocol.js +151 -0
- package/dist/federation/messages.d.ts +54 -0
- package/dist/federation/messages.js +35 -0
- package/dist/federation/ports.d.ts +287 -0
- package/dist/federation/ports.js +5 -0
- package/dist/federation/presets.d.ts +66 -0
- package/dist/federation/presets.js +30 -0
- package/dist/federation/refusal-notice.d.ts +198 -0
- package/dist/federation/refusal-notice.js +193 -0
- package/dist/federation/registrations.d.ts +171 -0
- package/dist/federation/registrations.js +181 -0
- package/dist/federation/reload.d.ts +112 -0
- package/dist/federation/reload.js +110 -0
- package/dist/federation/stdio/adapter.stdio.d.ts +157 -0
- package/dist/federation/stdio/adapter.stdio.js +526 -0
- package/dist/federation/stdio/default-connect.d.ts +29 -0
- package/dist/federation/stdio/default-connect.js +92 -0
- package/dist/federation/stdio/index.d.ts +4 -0
- package/dist/federation/stdio/index.js +4 -0
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +139 -0
- package/dist/federation/stdio/stdio-launch-resolver.js +227 -0
- package/dist/federation/testing/adapter.memory.d.ts +177 -0
- package/dist/federation/testing/adapter.memory.js +189 -0
- package/dist/federation/testing/index.d.ts +3 -0
- package/dist/federation/testing/index.js +3 -0
- package/dist/federation/testing/tool-approvals.memory.d.ts +27 -0
- package/dist/federation/testing/tool-approvals.memory.js +38 -0
- package/dist/federation/tool-approvals.d.ts +97 -0
- package/dist/federation/tool-approvals.js +42 -0
- package/dist/federation/trust.d.ts +471 -0
- package/dist/federation/trust.js +707 -0
- package/dist/index.d.ts +175 -2
- package/dist/index.js +172 -2
- package/dist/server/daemon-client.d.ts +16 -4
- package/dist/server/daemon-client.js +41 -19
- package/dist/server/ports.d.ts +9 -0
- package/dist/server/ports.js +2 -0
- package/dist/server/resource-protocol.d.ts +14 -4
- package/dist/server/resource-protocol.js +6 -6
- package/dist/server/resources/active-resource.js +3 -3
- package/dist/server/sdk-transport.d.ts +11 -0
- package/dist/server/sdk-transport.js +31 -0
- package/dist/server/tool-protocol.d.ts +31 -9
- package/dist/server/tool-protocol.js +20 -18
- package/dist/server/tool-server.d.ts +60 -43
- package/dist/server/tool-server.js +51 -27
- package/dist/server/tools/component-catalog-tools.js +7 -7
- package/dist/server/tools/delegated-tool.d.ts +7 -4
- package/dist/server/tools/delegated-tool.js +18 -16
- package/dist/server/tools/run-tools.js +15 -15
- package/dist/server/tools/tool-catalog-tools.js +7 -7
- package/dist/tools/ask-choice/index.d.ts +123 -0
- package/dist/tools/ask-choice/index.js +98 -0
- package/dist/tools/ask-choice/pending.d.ts +11 -0
- package/dist/tools/ask-choice/pending.js +34 -0
- package/dist/tools/ask-choice/question.d.ts +16 -0
- package/dist/tools/ask-choice/question.js +60 -0
- package/dist/tools/ask-choice/schema.d.ts +91 -0
- package/dist/tools/ask-choice/schema.js +27 -0
- package/dist/tools/ask-choice/types.d.ts +106 -0
- package/dist/tools/ask-choice/types.js +2 -0
- package/package.json +43 -5
- package/dist/core/oauth.d.ts +0 -274
- package/dist/core/oauth.js +0 -667
- package/dist/core/secure-write.d.ts +0 -17
- package/dist/core/secure-write.js +0 -72
package/dist/index.d.ts
CHANGED
|
@@ -7,11 +7,177 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Re-exports are explicit and named (no `export *`) so the public surface is
|
|
9
9
|
* visible here and name collisions surface at build time.
|
|
10
|
+
|
|
11
|
+
* Archived provenance rationale:
|
|
12
|
+
* ## What was dropped (and why)
|
|
13
|
+
*
|
|
14
|
+
* - **`live-artifacts/` (entire subdir)** — the OD per-run "live artifacts" MCP
|
|
15
|
+
* tool surface. An OD-product feature, out of scope per the port brief.
|
|
16
|
+
* - **The `od mcp` stdio server** in `client/client.ts` — `runMcpStdio`,
|
|
17
|
+
* `TOOL_DEFS`, `handleMcpToolCall`, and every tool handler (`get_artifact`,
|
|
18
|
+
* `create_project`, `start_run`, project/active-context resolution, studio deep
|
|
19
|
+
* links, run polling, …). This is not a generic MCP client: it is an
|
|
20
|
+
* OD-product MCP **server** hardwired to Open Design's REST API
|
|
21
|
+
* (`/api/projects`, `/api/skills`, `/api/design-systems`, `/api/runs`, …) and
|
|
22
|
+
* concepts (design systems, skills, plugins, artifacts, runs, studio, the
|
|
23
|
+
* `od://` resource scheme). It also imports the OD-only `@open-design/contracts`
|
|
24
|
+
* package (`buildProjectRawFileUrl`), the OD daemon-internal
|
|
25
|
+
* `../../artifacts/create.js`, and the `@modelcontextprotocol/sdk` — none of
|
|
26
|
+
* which exist in this repo. Faithfully porting it would mean importing OD
|
|
27
|
+
* product surface, so it was dropped and only its generic primitives salvaged.
|
|
28
|
+
* - **`MCP_TEMPLATES` catalog + template types + `templateId`** in `config.ts` —
|
|
29
|
+
* a large, design-tool-tilted curated catalog of third-party MCP servers with
|
|
30
|
+
* OD marketing prose. Zero functional coupling; dropped for neutrality (same
|
|
31
|
+
* rationale `@jini/sqlite` used to drop the OD "templates" table).
|
|
32
|
+
*
|
|
33
|
+
* ### The `list_agents` decision
|
|
34
|
+
*
|
|
35
|
+
* Investigated before building anything, per the task brief. `@jini/agent-runtime`'s
|
|
36
|
+
* barrel (`packages/agent-runtime/src/index.ts` → `export * from './registry.js'`)
|
|
37
|
+
* already exports `AGENT_DEFS: RuntimeAgentDef[]` — the exact static, in-memory
|
|
38
|
+
* list of every registered agent def (`registry.ts`'s `BASE_AGENT_DEFS`, 24
|
|
39
|
+
* entries). `RuntimeAgentDef` has plain `id`/`name` fields (plus a large surface
|
|
40
|
+
* of CLI-spawn internals: `bin`, `buildArgs`, `env`, `listModels`, etc. — no
|
|
41
|
+
* `available`/live-detection field at all). No HTTP projection of this list
|
|
42
|
+
* existed anywhere in `packages/http/src/*.ts` before this pass. Building
|
|
43
|
+
* `GET /api/agents` returning `{agents: [{id, name}]}` needed no design judgment
|
|
44
|
+
* call beyond "project the two safe fields" — no timeout/caching/probing
|
|
45
|
+
* decisions, unlike OD's own `list_agents` (`listAgents`, mcp.ts:1034-1057),
|
|
46
|
+
* which filters on a *live-detected* `available: boolean` field (OD's
|
|
47
|
+
* `/api/agents` route actually probes each agent binary for installation/version
|
|
48
|
+
* at request time — a real design decision this pass deliberately did not need
|
|
49
|
+
* to make, since Jini has no such probing route or field to project). So it was
|
|
50
|
+
* built: `packages/http/src/agents.ts`'s `agentListRoute` (`GET /api/agents`,
|
|
51
|
+
* `AgentsHttpDeps.listAgents: () => readonly {id, name}[]` injected — DI,
|
|
52
|
+
* matching `daemon-status.ts`/`active-context.ts`'s convention, so `@jini/http`
|
|
53
|
+
* does not take on a new `@jini/agent-runtime` package dependency for a
|
|
54
|
+
* two-field projection) plus `@jini/mcp`'s `list_agents` tool
|
|
55
|
+
* (`server/tools/run-tools.ts`) proxying it. OD's `includeUnavailable` flag and
|
|
56
|
+
* per-agent `installUrl`/`modelsCount` enrichment were **not** ported — those
|
|
57
|
+
* exist only because OD's route does live probing, which this pass's route does
|
|
58
|
+
* not attempt.
|
|
59
|
+
*
|
|
60
|
+
* ### What was explicitly excluded (and why)
|
|
61
|
+
*
|
|
62
|
+
* Per the task brief, only the mechanism plus tools mapping to real kernel
|
|
63
|
+
* primitives were ported. The other 13 of OD's 18 `TOOL_DEFS` were **not**
|
|
64
|
+
* ported — all are OD's project/file/artifact/skill/plugin model, none of which
|
|
65
|
+
* exists in the Jini kernel (extraction-plan §2.1's "no Project, Conversation,
|
|
66
|
+
* Brand, DesignSystem, Plugin/marketplace, or Automation noun" holds here
|
|
67
|
+
* exactly as it did for `@jini/cli`'s own `mcp` row):
|
|
68
|
+
* `list_projects`, `get_artifact`, `get_project`, `get_file`, `search_files`,
|
|
69
|
+
* `list_files`, `create_artifact`, `write_file`, `delete_file`, `delete_project`,
|
|
70
|
+
* `create_project`, `list_skills`, `list_plugins`. Also not ported: the
|
|
71
|
+
* `ListResourcesRequestSchema`/`ReadResourceRequestSchema` resource surface
|
|
72
|
+
* (`od://focus/active`, `od://skills/...`, `od://design-systems/...` — resources
|
|
73
|
+
* require a skill/design-system store this kernel doesn't have), the studio
|
|
74
|
+
* deep-link builder (`buildStudioUrl`, `getDefaultConversationId`,
|
|
75
|
+
* `getWebBaseUrl`), the project-name-resolution cache (`resolveProjectId`,
|
|
76
|
+
* `fetchProjectList`, `resolveProjectArg`), and the SSE-transcript reassembly
|
|
77
|
+
* (`fetchRunAgentMessage`) — every one of these exists only in service of a
|
|
78
|
+
* dropped tool.
|
|
79
|
+
*
|
|
80
|
+
* ### Design decision: the hosting-mechanism shape
|
|
81
|
+
*
|
|
82
|
+
* `createMcpToolServer({name, version, tools, resolveBaseUrl, instructions?,
|
|
83
|
+
* idleMs?, fetchImpl?, stdin?, stdout?, createServer?, createTransport?})`
|
|
84
|
+
* returns `{run(): Promise<void>}`. Construction (tool-name-uniqueness
|
|
85
|
+
* validation via `buildToolIndex`) is synchronous and side-effect-free; all I/O
|
|
86
|
+
* — resolving the daemon URL, connecting the transport, serving requests —
|
|
87
|
+
* happens in `run()`. This is genuinely new kernel-adjacent surface (a new
|
|
88
|
+
* package capability, not a file port), justified per extraction-plan §7's
|
|
89
|
+
* two-consumer rule by the user's explicit request plus the stated expectation
|
|
90
|
+
* that Zana/Open-Marketing will each want their own product-specific MCP
|
|
91
|
+
* server built on the same hosting mechanism later — so the mechanism accepts
|
|
92
|
+
* an arbitrary, caller-registered `McpToolDef[]`, not the 3-5 tools this pass
|
|
93
|
+
* ships as its first real, working example.
|
|
94
|
+
*
|
|
95
|
+
* `McpServerLike`/`McpTransportLike` (in `tool-server.ts`) are narrow structural
|
|
96
|
+
* interfaces covering only what this module calls on a `Server` instance
|
|
97
|
+
* (`setRequestHandler`, `connect`) and a transport (`onmessage`, `onclose`,
|
|
98
|
+
* `close`) — not literal re-exports of the SDK's own types, which can't be
|
|
99
|
+
* satisfied by a plain object (the SDK's `Server` class has private fields, so
|
|
100
|
+
* it's nominally, not structurally, typed). The default `createServer`/
|
|
101
|
+
* `createTransport` factories construct the real SDK classes and cast through
|
|
102
|
+
* `unknown` to these narrower interfaces (the same `as unknown as X` pattern
|
|
103
|
+
* `core/oauth.ts` already uses to bridge undici's types); a caller may override
|
|
104
|
+
* either factory to inject a test double. This is what makes `run()`'s full
|
|
105
|
+
* orchestration — activity wrapping, message/close composition, the stdin-close
|
|
106
|
+
* race, idle-triggered shutdown — deterministically unit-testable without a
|
|
107
|
+
* real subprocess or real timers (see `tool-server.test.ts`), while one
|
|
108
|
+
* dedicated test (`'wires the real @modelcontextprotocol/sdk Server +
|
|
109
|
+
* StdioServerTransport when no factories are injected'`) still exercises the
|
|
110
|
+
* true default path end-to-end with real `node:stream` `PassThrough` streams,
|
|
111
|
+
* proving the DI seam isn't hiding a real-SDK integration bug.
|
|
112
|
+
*
|
|
113
|
+
* **Why `daemon-client.ts` is not `@jini/cli`'s `getJsonFromDaemon`/
|
|
114
|
+
* `postJsonToDaemon`.** The architectural brief called for reusing
|
|
115
|
+
* `@jini/cli`'s already-hardened daemon-fetch helpers "the way
|
|
116
|
+
* `packages/cli/src/run-command.ts` already does." Checked directly: those two
|
|
117
|
+
* functions map every failure — network-unreachable, non-2xx, oversized
|
|
118
|
+
* response — onto `process.exit()` (via `exitWithStructuredError`), which is
|
|
119
|
+
* correct for a one-shot CLI invocation but fatal for a long-lived stdio MCP
|
|
120
|
+
* server, where a single failed tool call must return an `{isError:true}` MCP
|
|
121
|
+
* result and the process must keep serving the next call. Routing around this
|
|
122
|
+
* by injecting an `exit` callback that throws instead of exiting was considered
|
|
123
|
+
* and rejected: it would require also capturing/re-parsing whatever
|
|
124
|
+
* `postJsonToDaemon` had already written through its `write` callback to
|
|
125
|
+
* reconstruct an error message, which is more convoluted than porting OD's own
|
|
126
|
+
* already-throw-based `getJson`/`postJson` mechanism with the same bounded-I/O
|
|
127
|
+
* hardening added. What **is** reused directly from `@jini/cli` (both pure,
|
|
128
|
+
* non-`process.exit` functions): `sanitizeUntrustedText` (redacts daemon-
|
|
129
|
+
* supplied error text before it reaches an MCP result) and, per the
|
|
130
|
+
* architecture note about `daemon-url.ts`, `resolveDaemonUrl`/
|
|
131
|
+
* `sanitizeDaemonUrlForDisplay` are available for a caller's `resolveBaseUrl`
|
|
132
|
+
* implementation to use directly (this package does not re-wrap them — a
|
|
133
|
+
* caller passes `() => resolveDaemonUrl({...})` straight into
|
|
134
|
+
* `createMcpToolServer`'s `resolveBaseUrl` option; no `packages/mcp`-local
|
|
135
|
+
* daemon-URL-resolution equivalent was built, since one already exists and nothing
|
|
136
|
+
* about MCP's use case differs from a CLI command's). This cross-import
|
|
137
|
+
* (`@jini/mcp` -> `@jini/cli`) is allowed by `scripts/check-engine-boundaries.ts`'s
|
|
138
|
+
* R7 rule: R7 only restricts a **locked** package (the extraction-plan §3
|
|
139
|
+
* fourteen) importing an **unlocked** one below `"stable"` status; `@jini/mcp`
|
|
140
|
+
* is itself unlocked (`UNLOCKED.md`), so it importing the locked, stable
|
|
141
|
+
* `@jini/cli` package is unrestricted. Confirmed via `pnpm guard` after wiring
|
|
142
|
+
* the dependency (see below) — zero violations.
|
|
143
|
+
*
|
|
144
|
+
* No SSRF hardening (`assertSafePublicUrl`/`createValidatingLookup`, the
|
|
145
|
+
* pattern `core/oauth.ts` uses for its own outbound fetches) was added to
|
|
146
|
+
* `daemon-client.ts` — deliberately: `assertSafePublicUrl` actively *rejects*
|
|
147
|
+
* loopback/private addresses, which is exactly where a real daemon lives. The
|
|
148
|
+
* daemon URL here is a caller-resolved, typically-loopback target the user
|
|
149
|
+
* already trusts enough to run (the same trust boundary `@jini/cli/http.ts`
|
|
150
|
+
* already accepts for the identical "fetch my own daemon" concern), not an
|
|
151
|
+
* attacker- or remote-metadata-controlled URL the way an external MCP server's
|
|
152
|
+
* OAuth endpoints are.
|
|
153
|
+
*
|
|
154
|
+
* ### `daemonCallOptions(ctx)` — why a helper for two fields
|
|
155
|
+
*
|
|
156
|
+
* Nine call sites across `tools/` and `resources/` each wrote `{ fetchImpl: ctx.fetchImpl }` inline. Adding
|
|
157
|
+
* a second field that must travel with the first creates a failure mode that is *not* a compile error: miss
|
|
158
|
+
* one site and that single tool 401s while its neighbours keep working, which reaches a user as "some of
|
|
159
|
+
* these tools just don't do anything". Every site now routes through one mapping, so a newly added tool is
|
|
160
|
+
* authenticated by construction.
|
|
161
|
+
*
|
|
162
|
+
* The guarantee is a test rather than a convention. `server/__tests__/auth-propagation.test.ts` enumerates
|
|
163
|
+
* the **real** registries (`RUN_TOOLS`, `TOOL_CATALOG_TOOLS`, `createExecuteDelegatedToolTool`,
|
|
164
|
+
* `KERNEL_RESOURCES` — 8 tools + 1 resource), drives each handler through a spy `fetch`, and asserts the
|
|
165
|
+
* header on every request each one makes; a matching pass asserts **no** `Authorization` key at all when no
|
|
166
|
+
* credential was issued. It also asserts the surface list itself, so emptying or renaming a registry cannot
|
|
167
|
+
* make the loops vacuously pass.
|
|
168
|
+
*
|
|
169
|
+
* That test was verified to actually catch the bug it exists for: reverting one call site to the inline form
|
|
170
|
+
* fails it, naming the offending tool and URL.
|
|
171
|
+
*
|
|
172
|
+
* No transport change was needed — `daemon-client.ts`'s `DaemonRequestOptions` already carried `headers`.
|
|
173
|
+
*
|
|
174
|
+
* **Verified, personally, this session**: `pnpm --filter @jini-ai/mcp run typecheck` clean; full suite
|
|
175
|
+
* **332/332 passing** (18 files), including 21 new propagation tests and 4 new `serve()` credential tests.
|
|
176
|
+
* The pre-existing tool/resource suites, which assert the exact options object passed to the daemon client,
|
|
177
|
+
* pass **unmodified** — the evidence that the no-credential path is byte-identical.
|
|
10
178
|
*/
|
|
11
179
|
export { inferMcpAuthModeForUrl, sanitizeMcpServer, sanitizeMcpConfig, readMcpConfig, writeMcpConfig, isManagedProjectCwd, buildClaudeMcpJson, buildAcpMcpServers, buildOpenCodeMcpConfigContent, } from './core/index.js';
|
|
12
180
|
export type { McpTransport, McpAuthMode, McpServerConfig, McpConfig, AcpMcpServer, OpenCodeConfigBuildOptions, } from './core/index.js';
|
|
13
|
-
export { generateCodeVerifier, deriveCodeChallenge, generateState, discoverProtectedResource, discoverAuthServer, registerClient, getOrRegisterClient, buildAuthorizeUrl, exchangeCodeForToken, refreshAccessToken, PendingAuthCache, beginAuth, } from './core/index.js';
|
|
14
|
-
export type { ProtectedResourceMetadata, AuthorizationServerMetadata, RegisteredClient, OAuthTokenResponse, PendingAuthState, AuthorizeUrlInput, ExchangeCodeInput, RefreshTokenInput, BeginAuthInput, BeginAuthResult, } from './core/index.js';
|
|
15
181
|
export { sanitizeTokensFile, readTokensFile, getToken, setToken, clearToken, readAllTokens, isTokenExpired, } from './core/index.js';
|
|
16
182
|
export type { StoredMcpToken, McpTokensFile } from './core/index.js';
|
|
17
183
|
export { buildMcpInstallPayload } from './core/index.js';
|
|
@@ -27,4 +193,11 @@ export { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool,
|
|
|
27
193
|
export type { CreateExecuteDelegatedToolToolOptions } from './server/index.js';
|
|
28
194
|
export { AGENT_SLUGS, isAgentSlug, planAgentInstall, applyJsonInstall, removeJsonInstall, } from './agent-install/index.js';
|
|
29
195
|
export type { AgentSlug, McpLaunchSpec, PlanContext, CliInstallPlan, JsonInstallPlan, ManualInstallPlan, InstallPlan, } from './agent-install/index.js';
|
|
196
|
+
export type { McpConfigFilesystemPort } from './core/config.js';
|
|
197
|
+
export type { McpTokenClockOptions, McpTokenStoreOptions } from './core/tokens.js';
|
|
198
|
+
export type { BuildMcpInstallPayloadOptions } from './core/install-info.js';
|
|
199
|
+
export type { McpIdleTimerPort } from './client/client.js';
|
|
200
|
+
export type { McpToolServerRequiredArgs } from './server/tool-server.js';
|
|
201
|
+
export type { CreateExecuteDelegatedToolToolRequiredArgs } from './server/tools/delegated-tool.js';
|
|
202
|
+
export { DaemonHttpError } from './server/daemon-client.js';
|
|
30
203
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,180 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Re-exports are explicit and named (no `export *`) so the public surface is
|
|
9
9
|
* visible here and name collisions surface at build time.
|
|
10
|
+
|
|
11
|
+
* Archived provenance rationale:
|
|
12
|
+
* ## What was dropped (and why)
|
|
13
|
+
*
|
|
14
|
+
* - **`live-artifacts/` (entire subdir)** — the OD per-run "live artifacts" MCP
|
|
15
|
+
* tool surface. An OD-product feature, out of scope per the port brief.
|
|
16
|
+
* - **The `od mcp` stdio server** in `client/client.ts` — `runMcpStdio`,
|
|
17
|
+
* `TOOL_DEFS`, `handleMcpToolCall`, and every tool handler (`get_artifact`,
|
|
18
|
+
* `create_project`, `start_run`, project/active-context resolution, studio deep
|
|
19
|
+
* links, run polling, …). This is not a generic MCP client: it is an
|
|
20
|
+
* OD-product MCP **server** hardwired to Open Design's REST API
|
|
21
|
+
* (`/api/projects`, `/api/skills`, `/api/design-systems`, `/api/runs`, …) and
|
|
22
|
+
* concepts (design systems, skills, plugins, artifacts, runs, studio, the
|
|
23
|
+
* `od://` resource scheme). It also imports the OD-only `@open-design/contracts`
|
|
24
|
+
* package (`buildProjectRawFileUrl`), the OD daemon-internal
|
|
25
|
+
* `../../artifacts/create.js`, and the `@modelcontextprotocol/sdk` — none of
|
|
26
|
+
* which exist in this repo. Faithfully porting it would mean importing OD
|
|
27
|
+
* product surface, so it was dropped and only its generic primitives salvaged.
|
|
28
|
+
* - **`MCP_TEMPLATES` catalog + template types + `templateId`** in `config.ts` —
|
|
29
|
+
* a large, design-tool-tilted curated catalog of third-party MCP servers with
|
|
30
|
+
* OD marketing prose. Zero functional coupling; dropped for neutrality (same
|
|
31
|
+
* rationale `@jini/sqlite` used to drop the OD "templates" table).
|
|
32
|
+
*
|
|
33
|
+
* ### The `list_agents` decision
|
|
34
|
+
*
|
|
35
|
+
* Investigated before building anything, per the task brief. `@jini/agent-runtime`'s
|
|
36
|
+
* barrel (`packages/agent-runtime/src/index.ts` → `export * from './registry.js'`)
|
|
37
|
+
* already exports `AGENT_DEFS: RuntimeAgentDef[]` — the exact static, in-memory
|
|
38
|
+
* list of every registered agent def (`registry.ts`'s `BASE_AGENT_DEFS`, 24
|
|
39
|
+
* entries). `RuntimeAgentDef` has plain `id`/`name` fields (plus a large surface
|
|
40
|
+
* of CLI-spawn internals: `bin`, `buildArgs`, `env`, `listModels`, etc. — no
|
|
41
|
+
* `available`/live-detection field at all). No HTTP projection of this list
|
|
42
|
+
* existed anywhere in `packages/http/src/*.ts` before this pass. Building
|
|
43
|
+
* `GET /api/agents` returning `{agents: [{id, name}]}` needed no design judgment
|
|
44
|
+
* call beyond "project the two safe fields" — no timeout/caching/probing
|
|
45
|
+
* decisions, unlike OD's own `list_agents` (`listAgents`, mcp.ts:1034-1057),
|
|
46
|
+
* which filters on a *live-detected* `available: boolean` field (OD's
|
|
47
|
+
* `/api/agents` route actually probes each agent binary for installation/version
|
|
48
|
+
* at request time — a real design decision this pass deliberately did not need
|
|
49
|
+
* to make, since Jini has no such probing route or field to project). So it was
|
|
50
|
+
* built: `packages/http/src/agents.ts`'s `agentListRoute` (`GET /api/agents`,
|
|
51
|
+
* `AgentsHttpDeps.listAgents: () => readonly {id, name}[]` injected — DI,
|
|
52
|
+
* matching `daemon-status.ts`/`active-context.ts`'s convention, so `@jini/http`
|
|
53
|
+
* does not take on a new `@jini/agent-runtime` package dependency for a
|
|
54
|
+
* two-field projection) plus `@jini/mcp`'s `list_agents` tool
|
|
55
|
+
* (`server/tools/run-tools.ts`) proxying it. OD's `includeUnavailable` flag and
|
|
56
|
+
* per-agent `installUrl`/`modelsCount` enrichment were **not** ported — those
|
|
57
|
+
* exist only because OD's route does live probing, which this pass's route does
|
|
58
|
+
* not attempt.
|
|
59
|
+
*
|
|
60
|
+
* ### What was explicitly excluded (and why)
|
|
61
|
+
*
|
|
62
|
+
* Per the task brief, only the mechanism plus tools mapping to real kernel
|
|
63
|
+
* primitives were ported. The other 13 of OD's 18 `TOOL_DEFS` were **not**
|
|
64
|
+
* ported — all are OD's project/file/artifact/skill/plugin model, none of which
|
|
65
|
+
* exists in the Jini kernel (extraction-plan §2.1's "no Project, Conversation,
|
|
66
|
+
* Brand, DesignSystem, Plugin/marketplace, or Automation noun" holds here
|
|
67
|
+
* exactly as it did for `@jini/cli`'s own `mcp` row):
|
|
68
|
+
* `list_projects`, `get_artifact`, `get_project`, `get_file`, `search_files`,
|
|
69
|
+
* `list_files`, `create_artifact`, `write_file`, `delete_file`, `delete_project`,
|
|
70
|
+
* `create_project`, `list_skills`, `list_plugins`. Also not ported: the
|
|
71
|
+
* `ListResourcesRequestSchema`/`ReadResourceRequestSchema` resource surface
|
|
72
|
+
* (`od://focus/active`, `od://skills/...`, `od://design-systems/...` — resources
|
|
73
|
+
* require a skill/design-system store this kernel doesn't have), the studio
|
|
74
|
+
* deep-link builder (`buildStudioUrl`, `getDefaultConversationId`,
|
|
75
|
+
* `getWebBaseUrl`), the project-name-resolution cache (`resolveProjectId`,
|
|
76
|
+
* `fetchProjectList`, `resolveProjectArg`), and the SSE-transcript reassembly
|
|
77
|
+
* (`fetchRunAgentMessage`) — every one of these exists only in service of a
|
|
78
|
+
* dropped tool.
|
|
79
|
+
*
|
|
80
|
+
* ### Design decision: the hosting-mechanism shape
|
|
81
|
+
*
|
|
82
|
+
* `createMcpToolServer({name, version, tools, resolveBaseUrl, instructions?,
|
|
83
|
+
* idleMs?, fetchImpl?, stdin?, stdout?, createServer?, createTransport?})`
|
|
84
|
+
* returns `{run(): Promise<void>}`. Construction (tool-name-uniqueness
|
|
85
|
+
* validation via `buildToolIndex`) is synchronous and side-effect-free; all I/O
|
|
86
|
+
* — resolving the daemon URL, connecting the transport, serving requests —
|
|
87
|
+
* happens in `run()`. This is genuinely new kernel-adjacent surface (a new
|
|
88
|
+
* package capability, not a file port), justified per extraction-plan §7's
|
|
89
|
+
* two-consumer rule by the user's explicit request plus the stated expectation
|
|
90
|
+
* that Zana/Open-Marketing will each want their own product-specific MCP
|
|
91
|
+
* server built on the same hosting mechanism later — so the mechanism accepts
|
|
92
|
+
* an arbitrary, caller-registered `McpToolDef[]`, not the 3-5 tools this pass
|
|
93
|
+
* ships as its first real, working example.
|
|
94
|
+
*
|
|
95
|
+
* `McpServerLike`/`McpTransportLike` (in `tool-server.ts`) are narrow structural
|
|
96
|
+
* interfaces covering only what this module calls on a `Server` instance
|
|
97
|
+
* (`setRequestHandler`, `connect`) and a transport (`onmessage`, `onclose`,
|
|
98
|
+
* `close`) — not literal re-exports of the SDK's own types, which can't be
|
|
99
|
+
* satisfied by a plain object (the SDK's `Server` class has private fields, so
|
|
100
|
+
* it's nominally, not structurally, typed). The default `createServer`/
|
|
101
|
+
* `createTransport` factories construct the real SDK classes and cast through
|
|
102
|
+
* `unknown` to these narrower interfaces (the same `as unknown as X` pattern
|
|
103
|
+
* `core/oauth.ts` already uses to bridge undici's types); a caller may override
|
|
104
|
+
* either factory to inject a test double. This is what makes `run()`'s full
|
|
105
|
+
* orchestration — activity wrapping, message/close composition, the stdin-close
|
|
106
|
+
* race, idle-triggered shutdown — deterministically unit-testable without a
|
|
107
|
+
* real subprocess or real timers (see `tool-server.test.ts`), while one
|
|
108
|
+
* dedicated test (`'wires the real @modelcontextprotocol/sdk Server +
|
|
109
|
+
* StdioServerTransport when no factories are injected'`) still exercises the
|
|
110
|
+
* true default path end-to-end with real `node:stream` `PassThrough` streams,
|
|
111
|
+
* proving the DI seam isn't hiding a real-SDK integration bug.
|
|
112
|
+
*
|
|
113
|
+
* **Why `daemon-client.ts` is not `@jini/cli`'s `getJsonFromDaemon`/
|
|
114
|
+
* `postJsonToDaemon`.** The architectural brief called for reusing
|
|
115
|
+
* `@jini/cli`'s already-hardened daemon-fetch helpers "the way
|
|
116
|
+
* `packages/cli/src/run-command.ts` already does." Checked directly: those two
|
|
117
|
+
* functions map every failure — network-unreachable, non-2xx, oversized
|
|
118
|
+
* response — onto `process.exit()` (via `exitWithStructuredError`), which is
|
|
119
|
+
* correct for a one-shot CLI invocation but fatal for a long-lived stdio MCP
|
|
120
|
+
* server, where a single failed tool call must return an `{isError:true}` MCP
|
|
121
|
+
* result and the process must keep serving the next call. Routing around this
|
|
122
|
+
* by injecting an `exit` callback that throws instead of exiting was considered
|
|
123
|
+
* and rejected: it would require also capturing/re-parsing whatever
|
|
124
|
+
* `postJsonToDaemon` had already written through its `write` callback to
|
|
125
|
+
* reconstruct an error message, which is more convoluted than porting OD's own
|
|
126
|
+
* already-throw-based `getJson`/`postJson` mechanism with the same bounded-I/O
|
|
127
|
+
* hardening added. What **is** reused directly from `@jini/cli` (both pure,
|
|
128
|
+
* non-`process.exit` functions): `sanitizeUntrustedText` (redacts daemon-
|
|
129
|
+
* supplied error text before it reaches an MCP result) and, per the
|
|
130
|
+
* architecture note about `daemon-url.ts`, `resolveDaemonUrl`/
|
|
131
|
+
* `sanitizeDaemonUrlForDisplay` are available for a caller's `resolveBaseUrl`
|
|
132
|
+
* implementation to use directly (this package does not re-wrap them — a
|
|
133
|
+
* caller passes `() => resolveDaemonUrl({...})` straight into
|
|
134
|
+
* `createMcpToolServer`'s `resolveBaseUrl` option; no `packages/mcp`-local
|
|
135
|
+
* daemon-URL-resolution equivalent was built, since one already exists and nothing
|
|
136
|
+
* about MCP's use case differs from a CLI command's). This cross-import
|
|
137
|
+
* (`@jini/mcp` -> `@jini/cli`) is allowed by `scripts/check-engine-boundaries.ts`'s
|
|
138
|
+
* R7 rule: R7 only restricts a **locked** package (the extraction-plan §3
|
|
139
|
+
* fourteen) importing an **unlocked** one below `"stable"` status; `@jini/mcp`
|
|
140
|
+
* is itself unlocked (`UNLOCKED.md`), so it importing the locked, stable
|
|
141
|
+
* `@jini/cli` package is unrestricted. Confirmed via `pnpm guard` after wiring
|
|
142
|
+
* the dependency (see below) — zero violations.
|
|
143
|
+
*
|
|
144
|
+
* No SSRF hardening (`assertSafePublicUrl`/`createValidatingLookup`, the
|
|
145
|
+
* pattern `core/oauth.ts` uses for its own outbound fetches) was added to
|
|
146
|
+
* `daemon-client.ts` — deliberately: `assertSafePublicUrl` actively *rejects*
|
|
147
|
+
* loopback/private addresses, which is exactly where a real daemon lives. The
|
|
148
|
+
* daemon URL here is a caller-resolved, typically-loopback target the user
|
|
149
|
+
* already trusts enough to run (the same trust boundary `@jini/cli/http.ts`
|
|
150
|
+
* already accepts for the identical "fetch my own daemon" concern), not an
|
|
151
|
+
* attacker- or remote-metadata-controlled URL the way an external MCP server's
|
|
152
|
+
* OAuth endpoints are.
|
|
153
|
+
*
|
|
154
|
+
* ### `daemonCallOptions(ctx)` — why a helper for two fields
|
|
155
|
+
*
|
|
156
|
+
* Nine call sites across `tools/` and `resources/` each wrote `{ fetchImpl: ctx.fetchImpl }` inline. Adding
|
|
157
|
+
* a second field that must travel with the first creates a failure mode that is *not* a compile error: miss
|
|
158
|
+
* one site and that single tool 401s while its neighbours keep working, which reaches a user as "some of
|
|
159
|
+
* these tools just don't do anything". Every site now routes through one mapping, so a newly added tool is
|
|
160
|
+
* authenticated by construction.
|
|
161
|
+
*
|
|
162
|
+
* The guarantee is a test rather than a convention. `server/__tests__/auth-propagation.test.ts` enumerates
|
|
163
|
+
* the **real** registries (`RUN_TOOLS`, `TOOL_CATALOG_TOOLS`, `createExecuteDelegatedToolTool`,
|
|
164
|
+
* `KERNEL_RESOURCES` — 8 tools + 1 resource), drives each handler through a spy `fetch`, and asserts the
|
|
165
|
+
* header on every request each one makes; a matching pass asserts **no** `Authorization` key at all when no
|
|
166
|
+
* credential was issued. It also asserts the surface list itself, so emptying or renaming a registry cannot
|
|
167
|
+
* make the loops vacuously pass.
|
|
168
|
+
*
|
|
169
|
+
* That test was verified to actually catch the bug it exists for: reverting one call site to the inline form
|
|
170
|
+
* fails it, naming the offending tool and URL.
|
|
171
|
+
*
|
|
172
|
+
* No transport change was needed — `daemon-client.ts`'s `DaemonRequestOptions` already carried `headers`.
|
|
173
|
+
*
|
|
174
|
+
* **Verified, personally, this session**: `pnpm --filter @jini-ai/mcp run typecheck` clean; full suite
|
|
175
|
+
* **332/332 passing** (18 files), including 21 new propagation tests and 4 new `serve()` credential tests.
|
|
176
|
+
* The pre-existing tool/resource suites, which assert the exact options object passed to the daemon client,
|
|
177
|
+
* pass **unmodified** — the evidence that the no-credential path is byte-identical.
|
|
10
178
|
*/
|
|
179
|
+
// The original root-only boundary above describes the initial core/client/server split.
|
|
180
|
+
// Stable public subpaths now also host federation, approvals, stdio, testing, and ask-choice;
|
|
181
|
+
// internal source paths remain private so consumers can follow their declared runtime entry.
|
|
11
182
|
// ── core: config schema + IO ────────────────────────────────────────────────
|
|
12
183
|
export { inferMcpAuthModeForUrl, sanitizeMcpServer, sanitizeMcpConfig, readMcpConfig, writeMcpConfig, isManagedProjectCwd, buildClaudeMcpJson, buildAcpMcpServers, buildOpenCodeMcpConfigContent, } from './core/index.js';
|
|
13
|
-
// ── core: OAuth 2.1 / PKCE flow ─────────────────────────────────────────────
|
|
14
|
-
export { generateCodeVerifier, deriveCodeChallenge, generateState, discoverProtectedResource, discoverAuthServer, registerClient, getOrRegisterClient, buildAuthorizeUrl, exchangeCodeForToken, refreshAccessToken, PendingAuthCache, beginAuth, } from './core/index.js';
|
|
15
184
|
// ── core: token store ───────────────────────────────────────────────────────
|
|
16
185
|
export { sanitizeTokensFile, readTokensFile, getToken, setToken, clearToken, readAllTokens, isTokenExpired, } from './core/index.js';
|
|
17
186
|
// ── core: install-info payload ──────────────────────────────────────────────
|
|
@@ -30,4 +199,5 @@ export { searchComponentsTool, describeComponentTool, COMPONENT_CATALOG_TOOLS }
|
|
|
30
199
|
export { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool, DEFAULT_DELEGATED_TOOL_TIMEOUT_MS, } from './server/index.js';
|
|
31
200
|
// ── agent-install: register an MCP server into external agents ──────────────
|
|
32
201
|
export { AGENT_SLUGS, isAgentSlug, planAgentInstall, applyJsonInstall, removeJsonInstall, } from './agent-install/index.js';
|
|
202
|
+
export { DaemonHttpError } from './server/daemon-client.js';
|
|
33
203
|
//# sourceMappingURL=index.js.map
|
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class DaemonHttpError extends Error {
|
|
7
7
|
readonly status: number;
|
|
8
|
-
constructor(message
|
|
8
|
+
constructor({ message, status }: {
|
|
9
|
+
message: string;
|
|
10
|
+
status: number;
|
|
11
|
+
});
|
|
9
12
|
}
|
|
10
13
|
/** Thrown internally when a response exceeds its byte cap; always translated to a plain `Error` before it crosses this module's public functions. */
|
|
11
14
|
export declare class DaemonResponseTooLargeError extends Error {
|
|
12
15
|
readonly limitBytes: number;
|
|
13
|
-
constructor(limitBytes:
|
|
16
|
+
constructor({ limitBytes }: {
|
|
17
|
+
limitBytes: number;
|
|
18
|
+
});
|
|
14
19
|
}
|
|
15
20
|
export interface DaemonRequestOptions {
|
|
16
21
|
/** Defaults to the global `fetch`; inject for tests. */
|
|
@@ -24,7 +29,14 @@ export interface DaemonRequestOptions {
|
|
|
24
29
|
signal?: AbortSignal;
|
|
25
30
|
}
|
|
26
31
|
/** `GET <baseUrl><route>` and return the parsed JSON response, throwing on network failure or a non-2xx status. */
|
|
27
|
-
export declare function getDaemonJson<T = unknown>(baseUrl
|
|
32
|
+
export declare function getDaemonJson<T = unknown>({ baseUrl, route }: {
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
route: string;
|
|
35
|
+
}, options?: DaemonRequestOptions): Promise<T>;
|
|
28
36
|
/** `POST body` (JSON-serialized) to `<baseUrl><route>` and return the parsed JSON response, throwing on network failure or a non-2xx status. */
|
|
29
|
-
export declare function postDaemonJson<T = unknown>(baseUrl
|
|
37
|
+
export declare function postDaemonJson<T = unknown>({ baseUrl, route, body }: {
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
route: string;
|
|
40
|
+
body: unknown;
|
|
41
|
+
}, options?: DaemonRequestOptions): Promise<T>;
|
|
30
42
|
//# sourceMappingURL=daemon-client.d.ts.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (`../client/client.js`'s `createMcpIdleExitController` is the other half of
|
|
7
7
|
* the mechanism; this module is the network half).
|
|
8
8
|
*
|
|
9
|
-
* Deliberately NOT
|
|
9
|
+
* Deliberately NOT the CLI transport's `getJsonFromDaemon`/`postJsonToDaemon`
|
|
10
10
|
* (`packages/cli/src/http.ts`): those map a failure onto `process.exit`,
|
|
11
11
|
* which is the right contract for a one-shot CLI invocation but wrong here —
|
|
12
12
|
* a stdio MCP server is a long-lived process serving many tool calls, and one
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
* already-redacted `Error` whose message a tool handler's caller
|
|
20
20
|
* (`handleToolCall` in `./tool-protocol.js`) turns into that MCP error result.
|
|
21
21
|
*
|
|
22
|
-
* No SSRF hardening here
|
|
22
|
+
* No SSRF hardening here: the
|
|
23
23
|
* target is a caller-resolved, typically-loopback daemon the user already
|
|
24
24
|
* trusts enough to run — not an attacker- or server-metadata-controlled
|
|
25
25
|
* remote URL the way a configured external MCP server's OAuth endpoints are.
|
|
26
|
-
* This mirrors
|
|
26
|
+
* This mirrors the CLI HTTP transport's own posture for the identical "fetch my
|
|
27
27
|
* own daemon" concern (no `assertSafePublicUrl` there either).
|
|
28
28
|
*/
|
|
29
|
-
import { sanitizeUntrustedText } from '@jini-ai/
|
|
29
|
+
import { sanitizeUntrustedText } from '@jini-ai/core/text';
|
|
30
30
|
/** Request deadline. Generous enough for a slow tool call, short enough that a stalled daemon doesn't hang a stdio server turn forever. */
|
|
31
31
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
32
32
|
/** Cap on a daemon response body. Comfortably larger than any real status/run envelope, small enough to bound worst-case memory from a hostile/broken daemon. */
|
|
@@ -38,7 +38,7 @@ const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
|
38
38
|
*/
|
|
39
39
|
export class DaemonHttpError extends Error {
|
|
40
40
|
status;
|
|
41
|
-
constructor(message, status) {
|
|
41
|
+
constructor({ message, status }) {
|
|
42
42
|
super(message);
|
|
43
43
|
this.status = status;
|
|
44
44
|
this.name = 'DaemonHttpError';
|
|
@@ -47,7 +47,7 @@ export class DaemonHttpError extends Error {
|
|
|
47
47
|
/** Thrown internally when a response exceeds its byte cap; always translated to a plain `Error` before it crosses this module's public functions. */
|
|
48
48
|
export class DaemonResponseTooLargeError extends Error {
|
|
49
49
|
limitBytes;
|
|
50
|
-
constructor(limitBytes) {
|
|
50
|
+
constructor({ limitBytes }) {
|
|
51
51
|
super(`daemon response exceeded the ${limitBytes}-byte limit`);
|
|
52
52
|
this.limitBytes = limitBytes;
|
|
53
53
|
this.name = 'DaemonResponseTooLargeError';
|
|
@@ -76,7 +76,7 @@ async function readJsonWithLimit(resp, maxBytes) {
|
|
|
76
76
|
if (contentLength !== null && contentLength !== undefined) {
|
|
77
77
|
const declared = Number(contentLength);
|
|
78
78
|
if (Number.isFinite(declared) && declared > maxBytes)
|
|
79
|
-
throw new DaemonResponseTooLargeError(maxBytes);
|
|
79
|
+
throw new DaemonResponseTooLargeError({ limitBytes: maxBytes });
|
|
80
80
|
}
|
|
81
81
|
const body = resp.body;
|
|
82
82
|
if (body != null && typeof body.getReader === 'function') {
|
|
@@ -92,7 +92,7 @@ async function readJsonWithLimit(resp, maxBytes) {
|
|
|
92
92
|
if (value !== undefined) {
|
|
93
93
|
total += value.byteLength;
|
|
94
94
|
if (total > maxBytes)
|
|
95
|
-
throw new DaemonResponseTooLargeError(maxBytes);
|
|
95
|
+
throw new DaemonResponseTooLargeError({ limitBytes: maxBytes });
|
|
96
96
|
text += decoder.decode(value, { stream: true });
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -105,7 +105,7 @@ async function readJsonWithLimit(resp, maxBytes) {
|
|
|
105
105
|
}
|
|
106
106
|
const text = await resp.text();
|
|
107
107
|
if (Buffer.byteLength(text, 'utf8') > maxBytes)
|
|
108
|
-
throw new DaemonResponseTooLargeError(maxBytes);
|
|
108
|
+
throw new DaemonResponseTooLargeError({ limitBytes: maxBytes });
|
|
109
109
|
return parseJsonLoose(text);
|
|
110
110
|
}
|
|
111
111
|
/** Formats a `fetch()` rejection (network-unreachable) into a message safe to surface as an MCP tool error. */
|
|
@@ -115,18 +115,40 @@ function formatConnectionFailure(err, baseUrl) {
|
|
|
115
115
|
? cause.code
|
|
116
116
|
: null;
|
|
117
117
|
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND') {
|
|
118
|
-
return `cannot reach the daemon at ${baseUrl}. Is it running
|
|
118
|
+
return safeDiagnostic({ text: `cannot reach the daemon at ${baseUrl}. Is it running?` });
|
|
119
119
|
}
|
|
120
120
|
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
-
return
|
|
121
|
+
return safeDiagnostic({ text: message });
|
|
122
|
+
}
|
|
123
|
+
/** Remove URL credentials, queries and fragments before ordinary secret/control redaction.
|
|
124
|
+
* Even short query tokens are secrets; token-length heuristics cannot protect them.
|
|
125
|
+
* @complexity O(n) in diagnostic text, plus URL parsing for each URL.
|
|
126
|
+
*/
|
|
127
|
+
function safeDiagnostic({ text }) {
|
|
128
|
+
const redacted = text.replace(/https?:\/\/[^\s<>"']+/gi, url => {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(url);
|
|
131
|
+
if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
|
|
132
|
+
return url;
|
|
133
|
+
parsed.username = '';
|
|
134
|
+
parsed.password = '';
|
|
135
|
+
parsed.search = '';
|
|
136
|
+
parsed.hash = '';
|
|
137
|
+
return parsed.toString();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return '[redacted URL]';
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return sanitizeUntrustedText({ text: redacted });
|
|
122
144
|
}
|
|
123
145
|
/** Formats a non-2xx daemon response into a message safe to surface as an MCP tool error. */
|
|
124
146
|
function formatDaemonHttpError(status, url, data) {
|
|
125
147
|
const envelope = data;
|
|
126
|
-
const code = typeof envelope
|
|
127
|
-
const rawMessage = typeof envelope
|
|
128
|
-
const detail = rawMessage
|
|
129
|
-
return code !== undefined ? `daemon ${status} on ${url}: ${code}: ${detail}` : `daemon ${status} on ${url}: ${detail}
|
|
148
|
+
const code = typeof envelope?.error?.code === 'string' ? envelope.error.code : undefined;
|
|
149
|
+
const rawMessage = typeof envelope?.error?.message === 'string' ? envelope.error.message : undefined;
|
|
150
|
+
const detail = rawMessage ?? `HTTP ${status}`;
|
|
151
|
+
return safeDiagnostic({ text: code !== undefined ? `daemon ${status} on ${url}: ${code}: ${detail}` : `daemon ${status} on ${url}: ${detail}` });
|
|
130
152
|
}
|
|
131
153
|
/**
|
|
132
154
|
* Shared transport core behind {@link getDaemonJson} and {@link postDaemonJson}: fetch
|
|
@@ -165,12 +187,12 @@ async function requestDaemonJson(baseUrl, route, init, options = {}) {
|
|
|
165
187
|
}
|
|
166
188
|
catch (err) {
|
|
167
189
|
if (err instanceof DaemonResponseTooLargeError) {
|
|
168
|
-
throw new Error(`response from ${url} exceeded the ${err.limitBytes}-byte limit`);
|
|
190
|
+
throw new Error(safeDiagnostic({ text: `response from ${url} exceeded the ${err.limitBytes}-byte limit` }));
|
|
169
191
|
}
|
|
170
192
|
throw err;
|
|
171
193
|
}
|
|
172
194
|
if (!resp.ok) {
|
|
173
|
-
throw new DaemonHttpError(formatDaemonHttpError(resp.status, url, data), resp.status);
|
|
195
|
+
throw new DaemonHttpError({ message: formatDaemonHttpError(resp.status, url, data), status: resp.status });
|
|
174
196
|
}
|
|
175
197
|
return data;
|
|
176
198
|
}
|
|
@@ -179,11 +201,11 @@ async function requestDaemonJson(baseUrl, route, init, options = {}) {
|
|
|
179
201
|
}
|
|
180
202
|
}
|
|
181
203
|
/** `GET <baseUrl><route>` and return the parsed JSON response, throwing on network failure or a non-2xx status. */
|
|
182
|
-
export async function getDaemonJson(baseUrl, route, options) {
|
|
204
|
+
export async function getDaemonJson({ baseUrl, route }, options) {
|
|
183
205
|
return requestDaemonJson(baseUrl, route, { method: 'GET' }, options);
|
|
184
206
|
}
|
|
185
207
|
/** `POST body` (JSON-serialized) to `<baseUrl><route>` and return the parsed JSON response, throwing on network failure or a non-2xx status. */
|
|
186
|
-
export async function postDaemonJson(baseUrl, route, body, options) {
|
|
208
|
+
export async function postDaemonJson({ baseUrl, route, body }, options) {
|
|
187
209
|
return requestDaemonJson(baseUrl, route, { method: 'POST', body }, options);
|
|
188
210
|
}
|
|
189
211
|
//# sourceMappingURL=daemon-client.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Transport port. The default adapter translates SDK callbacks to object arguments. */
|
|
2
|
+
export interface McpTransportLike {
|
|
3
|
+
onmessage?: ((args: {
|
|
4
|
+
message: unknown;
|
|
5
|
+
}) => void) | undefined;
|
|
6
|
+
onclose?: (() => void) | undefined;
|
|
7
|
+
close(required: Record<string, never>): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -17,12 +17,18 @@ export interface McpResourceDef {
|
|
|
17
17
|
readonly name: string;
|
|
18
18
|
readonly description?: string;
|
|
19
19
|
readonly mimeType?: string;
|
|
20
|
-
readonly read: (
|
|
20
|
+
readonly read: (requiredArgs: {
|
|
21
|
+
ctx: McpToolContext;
|
|
22
|
+
}) => Promise<McpResourceReadResult> | McpResourceReadResult;
|
|
21
23
|
}
|
|
22
24
|
/** Projects a resource list into the `Resource[]` shape `resources/list` returns. */
|
|
23
|
-
export declare function resourcesToList(resources
|
|
25
|
+
export declare function resourcesToList({ resources }: {
|
|
26
|
+
resources: readonly McpResourceDef[];
|
|
27
|
+
}): Resource[];
|
|
24
28
|
/** Builds a uri -> def lookup, throwing if two resources in `resources` share a uri (a caller-configuration bug, surfaced eagerly at server-construction time rather than letting the second registration shadow the first). */
|
|
25
|
-
export declare function buildResourceIndex(resources
|
|
29
|
+
export declare function buildResourceIndex({ resources }: {
|
|
30
|
+
resources: readonly McpResourceDef[];
|
|
31
|
+
}): Map<string, McpResourceDef>;
|
|
26
32
|
/**
|
|
27
33
|
* `resources/read` dispatch: looks up `uri` in `resources` and runs its `read`. Unlike
|
|
28
34
|
* {@link import('./tool-protocol.js').handleToolCall}, a failure here *throws* rather than
|
|
@@ -34,5 +40,9 @@ export declare function buildResourceIndex(resources: readonly McpResourceDef[])
|
|
|
34
40
|
* it leaves this function, the same conservative posture `handleToolCall` uses — a `read`
|
|
35
41
|
* implementation may re-throw text that traces back to an untrusted daemon response.
|
|
36
42
|
*/
|
|
37
|
-
export declare function handleResourceRead(uri
|
|
43
|
+
export declare function handleResourceRead({ uri, resources, ctx }: {
|
|
44
|
+
uri: string;
|
|
45
|
+
resources: ReadonlyMap<string, McpResourceDef>;
|
|
46
|
+
ctx: McpToolContext;
|
|
47
|
+
}): Promise<ReadResourceResult>;
|
|
38
48
|
//# sourceMappingURL=resource-protocol.d.ts.map
|