@plurnk/plurnk-mcp 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -15
- package/SPEC.md +188 -92
- package/dist/McpExecutor.d.ts +10 -1
- package/dist/McpExecutor.d.ts.map +1 -1
- package/dist/McpExecutor.js +34 -8
- package/dist/McpExecutor.js.map +1 -1
- package/dist/McpResources.d.ts +2 -1
- package/dist/McpResources.d.ts.map +1 -1
- package/dist/McpResources.js +7 -3
- package/dist/McpResources.js.map +1 -1
- package/dist/Module.d.ts.map +1 -1
- package/dist/Module.js +142 -43
- package/dist/Module.js.map +1 -1
- package/dist/PluginConfiguration.d.ts +8 -10
- package/dist/PluginConfiguration.d.ts.map +1 -1
- package/dist/PluginConfiguration.js +4 -4
- package/dist/PluginConfiguration.js.map +1 -1
- package/dist/capabilityMatrix.d.ts.map +1 -1
- package/dist/capabilityMatrix.js +1 -0
- package/dist/capabilityMatrix.js.map +1 -1
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +174 -83
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/definition.js.map +1 -1
- package/dist/extensionChannel.d.ts.map +1 -1
- package/dist/extensionChannel.js +2 -3
- package/dist/extensionChannel.js.map +1 -1
- package/dist/inputRequired.d.ts.map +1 -1
- package/dist/inputRequired.js +11 -4
- package/dist/inputRequired.js.map +1 -1
- package/dist/oauth.d.ts +28 -4
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +83 -36
- package/dist/oauth.js.map +1 -1
- package/dist/protocolHeaders.d.ts.map +1 -1
- package/dist/protocolHeaders.js +3 -2
- package/dist/protocolHeaders.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -3
- package/dist/registry.js.map +1 -1
- package/dist/subscriptions.js.map +1 -1
- package/dist/tasks.d.ts +2 -2
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +23 -26
- package/dist/tasks.js.map +1 -1
- package/docs/mcp.md +9 -5
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -132,7 +132,8 @@ PLURNK_MCP_forge_ENABLED=0
|
|
|
132
132
|
|
|
133
133
|
HTTP authorization belongs in the complete definition. Bearer tokens and
|
|
134
134
|
OAuth client secrets are symbolic `${NAME}` environment references, resolved
|
|
135
|
-
only when preparing a connection; interactive
|
|
135
|
+
only when preparing a connection; an interactive sign-in stays in memory until
|
|
136
|
+
the daemon stops.
|
|
136
137
|
See [`.env.defaults`](./.env.defaults) for all controls and
|
|
137
138
|
[`SPEC.md`](./SPEC.md#mcp-configuration-configuration) for the contract.
|
|
138
139
|
|
|
@@ -186,26 +187,40 @@ client.
|
|
|
186
187
|
HTTP servers support bearer references, client credentials, and interactive
|
|
187
188
|
OAuth; stdio servers read their credentials from the environment. A server that
|
|
188
189
|
challenges an anonymous connection comes up `authorization-required`, without
|
|
189
|
-
publishing partial tools.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
publishing partial tools. One that answers its catalog anonymously and refuses a
|
|
191
|
+
request instead goes `authorization-required` at that refusal, and the request
|
|
192
|
+
fails with that code ({§mcp-request-unauthorized}); either way its alias stays
|
|
193
|
+
registered, out of service ({§mcp-unavailable-runtime}). A URL-only definition
|
|
194
|
+
is sufficient; no callback port needs to be configured. The terminal client's
|
|
195
|
+
`/mcp oauth <alias>` binds an available loopback port and calls
|
|
196
|
+
`workspace.mcp.oauth.begin` before opening the returned authorization URL. A
|
|
197
|
+
fixed OAuth redirect, if configured, is respected. A server that publishes no
|
|
198
|
+
OAuth metadata needs its authorization server named as `authorization.issuer`,
|
|
199
|
+
and `authorization.authorizationParams` adds provider request parameters such as
|
|
200
|
+
`access_type=offline` where a refresh token is issued only on request
|
|
201
|
+
({§oauth-issuer}). Supplying the callback URL explicitly also works for
|
|
202
|
+
remote/headless use. The client submits the complete URL through
|
|
203
|
+
`workspace.mcp.oauth.complete`. Its `202` acknowledges sign-in; tools activate
|
|
204
|
+
at the next safe workspace boundary, between turns, without another callback
|
|
205
|
+
submission. Until then `list` and a call to the alias report the published
|
|
206
|
+
state, so a turn in flight still sees the alias awaiting sign-in. A sign-in
|
|
207
|
+
outlives its connection: the alias's later connections with the same definition
|
|
208
|
+
present it, refreshing it as it lapses — when an idle workspace returns, on
|
|
209
|
+
`enable`, or on `disable` then `enable` — until its authorization server rejects
|
|
210
|
+
it, another sign-in replaces it, or the daemon stops ({§oauth-grant}). `list`
|
|
211
|
+
shows an active alias's grant as its `detail.grant`: its issuer, the scope its
|
|
212
|
+
authorization server stated, and whether it refreshes, never a token. PKCE,
|
|
198
213
|
issuer and resource validation, refresh, scope escalation, and credentials
|
|
199
|
-
remain inside the host
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
remain inside the host. Callback addresses and credentials never replace the
|
|
215
|
+
saved definition. `enable` retries a connection; it does not replace user
|
|
216
|
+
sign-in. Rejected configured credentials are reported as authentication failures
|
|
217
|
+
rather than retryable server downtime.
|
|
203
218
|
|
|
204
219
|
## Verification
|
|
205
220
|
|
|
206
221
|
```sh
|
|
207
222
|
npm test -w @plurnk/plurnk-mcp
|
|
208
|
-
PLURNK_MODEL=<selector> npm run test:live
|
|
223
|
+
PLURNK_MODEL=<selector> npm run test:live -- --specimen "live mcp: current third-party stdio and HTTP servers compose through the assembled product"
|
|
209
224
|
```
|
|
210
225
|
|
|
211
226
|
The package gate runs the exact current SDK and official conformance
|
package/SPEC.md
CHANGED
|
@@ -33,6 +33,15 @@ per-connection and absent from a legacy peer.
|
|
|
33
33
|
|
|
34
34
|
## §mcp-core-matrix Core capability matrix
|
|
35
35
|
|
|
36
|
+
<!-- map -->
|
|
37
|
+
| Tag | Promise |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| {§mcp-catalog-convergence} | A catalog is complete or it is an error. |
|
|
40
|
+
| {§mcp-catalog-deadline} | `PLURNK_MCP_CONNECT_TIMEOUT` bounds connection setup and, separately, each complete catalog/list walk,… |
|
|
41
|
+
| {§mcp-retry-pacing} | Every retry the adapter schedules on its own — reopening a dropped subscription, refreshing a catalog a… |
|
|
42
|
+
| {§mcp-catalog-list-absence} | An unsupported list method does not disable the server. |
|
|
43
|
+
<!-- /map -->
|
|
44
|
+
|
|
36
45
|
The accountable capability matrix lives in `capabilityMatrix.ts`
|
|
37
46
|
({§mcp-capability-matrix}). This table describes the pinned revision;
|
|
38
47
|
{§mcp-authority} owns the SDK-negotiated older-peer surface.
|
|
@@ -167,6 +176,15 @@ conformance stays a separate named gate, never folded into a matrix row.
|
|
|
167
176
|
|
|
168
177
|
## §mcp-transports Transport bindings
|
|
169
178
|
|
|
179
|
+
<!-- map -->
|
|
180
|
+
| Tag | Promise |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| {§mcp-endpoint-security} | Configured MCP and registry endpoints accept HTTP or HTTPS, including private-network hosts. |
|
|
183
|
+
| {§mcp-stdio-process-ownership} | A stdio connection owns the complete process group created for its server. |
|
|
184
|
+
| {§mcp-redirect-refused} | An HTTP endpoint is never redirected. |
|
|
185
|
+
| {§mcp-header-declarations} | A tool with an invalid `x-mcp-header` declaration is never called. |
|
|
186
|
+
<!-- /map -->
|
|
187
|
+
|
|
170
188
|
| Binding | Contract |
|
|
171
189
|
|---|---|
|
|
172
190
|
| stdio | Spawn one exact executable with an explicit argument array and no shell; newline-delimited JSON-RPC is the only stdout/stdin traffic; stderr is diagnostic; shutdown closes stdin, waits, then terminates if necessary |
|
|
@@ -194,17 +212,15 @@ case-insensitively, and body/header disagreement fails instead of guessing.
|
|
|
194
212
|
For `tasks/get`, `tasks/update`, and `tasks/cancel`, `Mcp-Name` is the encoded
|
|
195
213
|
`taskId` required by the Tasks extension.
|
|
196
214
|
|
|
197
|
-
§mcp-header-declarations **A tool with an invalid `x-mcp-header` declaration is never called.** Before
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
({§mcp-catalog-deadline}): with no definition at hand there is nothing to scan and no routing header to
|
|
207
|
-
derive, so it proceeds as a plain `tools/call`.
|
|
215
|
+
§mcp-header-declarations **A tool with an invalid `x-mcp-header` declaration is never called.** Before either request leg of a tool call, Tasks or direct, the tool's `inputSchema` is scanned for `x-mcp-header` declarations. A definition that breaks any rule below refuses the call with `tool-header-invalid` ({§problems-mcp}) naming the tool and the declaration, and no request is sent.
|
|
216
|
+
|
|
217
|
+
| Rule | What |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| Placement | each sits on a property reached through `properties` alone, never under `items`, `additionalProperties`, a combinator, a conditional, `$defs` or a `$ref` target |
|
|
220
|
+
| Name | a non-empty RFC 9110 token that is unique case-insensitively |
|
|
221
|
+
| Type | `string`, `integer` or `boolean`, never `number` (SEP-2243) |
|
|
222
|
+
| The definition scanned | the one the caller supplies; otherwise the server's listing supplies it, and a Tasks call whose listing lacks the tool fails as unlisted |
|
|
223
|
+
| A direct call | never waits on a failed listing ({§mcp-catalog-deadline}): with no definition at hand there is nothing to scan and no routing header to derive, so it proceeds as a plain `tools/call` |
|
|
208
224
|
|
|
209
225
|
## §mcp-errors Error allocation
|
|
210
226
|
|
|
@@ -226,6 +242,27 @@ originating distinction in its canonical Problem/result path.
|
|
|
226
242
|
|
|
227
243
|
## §mcp-configuration Configuration
|
|
228
244
|
|
|
245
|
+
<!-- map -->
|
|
246
|
+
| Tag | Promise |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| {§mcp-file-configuration} | Standalone `mcp.json` files are read-only configuration inputs, not plugins. |
|
|
249
|
+
| {§mcp-plugin-configuration} | Plugin MCP components use their format's interpretation, not the native file dialect. |
|
|
250
|
+
| {§mcp-activation-isolation} | Cold endpoint failure is capability-local. |
|
|
251
|
+
| {§mcp-unavailable-runtime} | An enabled server that cannot run keeps its alias. |
|
|
252
|
+
| {§mcp-definitions} | A server is not a plugin installation. |
|
|
253
|
+
| {§mcp-launch-directory} | An implicit working directory is workspace-owned state, not the project. |
|
|
254
|
+
| {§mcp-summary-derivation} | Orientation prefers the server's own purpose over display labels; no capabilities are inferred. |
|
|
255
|
+
| {§mcp-server-settings} | Independent behavior settings do not change connection definitions. |
|
|
256
|
+
| {§mcp-module} | The MCP family beneath the coordinator |
|
|
257
|
+
| {§mcp-launch-environment} | Ambient inheritance and explicit bindings are distinct. |
|
|
258
|
+
| {§mcp-management-actions} | MCP is one family of workspace Functionality |
|
|
259
|
+
| {§mcp-registry-discovery} | `discover` searches the MCP Registry. |
|
|
260
|
+
| {§mcp-preparation-diagnostics} | Unavailable preparation reports observations. |
|
|
261
|
+
| {§oauth-continuation} | A URL-only HTTP definition is sufficient to reach the client sign-in handoff. |
|
|
262
|
+
| {§oauth-issuer} | A configured issuer is where a sign-in starts and the one server it trusts. |
|
|
263
|
+
| {§oauth-begin-unchallenged} | A sign-in starts where no connection is challenged. |
|
|
264
|
+
<!-- /map -->
|
|
265
|
+
|
|
229
266
|
MCP servers are complete connection definitions ({§mcp-server-definition}). Standalone files, plugin components, and environment declarations
|
|
230
267
|
supply the service baseline; live additions belong to the workspace. All use the common resolution
|
|
231
268
|
and lifecycle ({§configuration-definition-resolution}, {§functionality-coordinator}). Disabled definitions remain
|
|
@@ -288,13 +325,23 @@ source repair replaces them, rather than retaining a historical warning.
|
|
|
288
325
|
|
|
289
326
|
§mcp-activation-isolation **Cold endpoint failure is capability-local.** An enabled server
|
|
290
327
|
that cannot connect or complete discovery during workspace activation remains
|
|
291
|
-
enabled and client-visible as `unavailable`. It publishes no
|
|
292
|
-
|
|
293
|
-
from starting or serving dormant workers. Enabling that already-enabled alias is an explicit reconnect
|
|
328
|
+
enabled and client-visible as `unavailable`. It publishes no tools, resources, or
|
|
329
|
+
documentation, only its alias out of service ({§mcp-unavailable-runtime}), and cannot
|
|
330
|
+
prevent other capabilities or the daemon from starting or serving dormant workers. Enabling that already-enabled alias is an explicit reconnect
|
|
294
331
|
attempt; failure preserves the unavailable snapshot, while success atomically
|
|
295
332
|
replaces it. An interactive enable continues to reject an unavailable candidate
|
|
296
333
|
without changing durable state.
|
|
297
334
|
|
|
335
|
+
§mcp-unavailable-runtime **An enabled server that cannot run keeps its alias.** Preparation registers
|
|
336
|
+
every enabled server: an active one with its tools and resources, any other as a runtime out of
|
|
337
|
+
service whose availability states what keeps it from running, so a call to the alias is refused with
|
|
338
|
+
that state ({§exec-registry-resolves}) and never as a name nothing registered.
|
|
339
|
+
|
|
340
|
+
| State | The refusal names |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| `authorization-required` | the operator's sign-in it awaits |
|
|
343
|
+
| `unavailable` | what preparation observed: its Problem's bounded diagnostic, else its detail |
|
|
344
|
+
|
|
298
345
|
| Variable | Contract |
|
|
299
346
|
|---|---|
|
|
300
347
|
| `PLURNK_MCP_<alias>` | Complete `McpServerDefinition` JSON; transport and authentication replace together |
|
|
@@ -395,30 +442,19 @@ environment change; there is no automatic restart or stale-configuration state.
|
|
|
395
442
|
have no local process environment; their native header and authorization references use the
|
|
396
443
|
same workspace-composed operator environment.
|
|
397
444
|
|
|
398
|
-
§mcp-management-actions MCP is one family of workspace Functionality ({§functionality-coordinator}). The
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
`
|
|
404
|
-
|
|
405
|
-
one outcome per alias (`active` with catalog detail — negotiated protocol version, server identity,
|
|
406
|
-
|
|
407
|
-
`
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
§mcp-registry-discovery **`discover` searches the MCP Registry.** The family serves discovery
|
|
412
|
-
while `PLURNK_MCP_REGISTRY_URL`, read when the module is constructed, names a registry; empty,
|
|
413
|
-
the family has no `discover` ({§functionality-discover-advertisement}). Its one input, `query`,
|
|
414
|
-
asks that registry (API v0.1) for one page of at most `PLURNK_MCP_REGISTRY_LIMIT` servers, each at
|
|
415
|
-
its latest version, whose names match. Each npm, PyPI, NuGet or OCI package with a stdio transport
|
|
416
|
-
becomes a stdio entry run as the registry's own examples run it (`npx -y`, `uvx`, `dnx`, or
|
|
417
|
-
`docker run -i --rm` passing each declared variable through), and each Streamable HTTP remote becomes a
|
|
418
|
-
URL entry with its non-secret literal headers. An entry that needs a person's input first, such as a
|
|
419
|
-
template variable or a required argument with no value, has none. A candidate is a complete definition;
|
|
420
|
-
its summary names the environment variables and headers the
|
|
421
|
-
server needs, and its provenance names the registry and the server's `name@version`.
|
|
445
|
+
§mcp-management-actions **MCP is one family of workspace Functionality** ({§functionality-coordinator}). The coordinator publishes `workspace.mcp.list | add | replace | enable | disable | remove`, with `discover` while a registry is configured, and the model's `mcp` runtime with the common semantics, durable state, and publication; this module registers the family adapter and owns protocol truth beneath it.
|
|
446
|
+
|
|
447
|
+
| Verb | What |
|
|
448
|
+
| --- | --- |
|
|
449
|
+
| `available` | the configured service baseline |
|
|
450
|
+
| `add`, `replace` ({§functionality-replacement}), `remove` | change workspace state ({§mcp-definitions}) |
|
|
451
|
+
| `discover` | searches the MCP Registry ({§mcp-registry-discovery}) |
|
|
452
|
+
| `prepare` | connects the enabled set, reusing unchanged live attachments, and returns one runtime per enabled server, with its resource facet when active ({§mcp-unavailable-runtime}), one outcome per alias (`active` with catalog detail — negotiated protocol version, server identity, capabilities, tool names, resource and prompt counts — and any grant it presents ({§oauth-grant}), `unavailable` with its exact Problem, or `authorization-required` with its URL), and a snapshot in three phases |
|
|
453
|
+
| `publish` | adopts the connected set and consumes or records pending authorizations, synchronously |
|
|
454
|
+
| `commit` | closes connections the new set no longer uses |
|
|
455
|
+
| `abort` | closes only what the attempt opened |
|
|
456
|
+
|
|
457
|
+
§mcp-registry-discovery **`discover` searches the MCP Registry.** The family serves discovery while `PLURNK_MCP_REGISTRY_URL`, read when the module is constructed, names a registry; empty, the family has no `discover` ({§functionality-discover-advertisement}). Its one input, `query`, asks that registry (API v0.1) for one page of at most `PLURNK_MCP_REGISTRY_LIMIT` servers, each at its latest version, whose names match.
|
|
422
458
|
|
|
423
459
|
| Discovery, admission, or installation condition | Problem | Status |
|
|
424
460
|
|---|---|---|
|
|
@@ -426,6 +462,13 @@ server needs, and its provenance names the registry and the server's `name@versi
|
|
|
426
462
|
| The complete connection definition is invalid | `definition-invalid` | 400 |
|
|
427
463
|
| The alias differs from the definition's name | `alias-mismatch` | 400 |
|
|
428
464
|
|
|
465
|
+
| Registry server | Candidate |
|
|
466
|
+
| --- | --- |
|
|
467
|
+
| Each npm, PyPI, NuGet or OCI package with a stdio transport | a stdio entry run as the registry's own examples run it (`npx -y`, `uvx`, `dnx`, or `docker run -i --rm` passing each declared variable through) |
|
|
468
|
+
| Each Streamable HTTP remote | a URL entry with its non-secret literal headers |
|
|
469
|
+
| One that needs a person's input first, such as a template variable or a required argument with no value | none |
|
|
470
|
+
| Every candidate | a complete definition; its summary names the environment variables and headers the server needs, and its provenance names the registry and the server's `name@version` |
|
|
471
|
+
|
|
429
472
|
Protocol continuations remain MCP-registered workspace actions beneath the
|
|
430
473
|
common grammar:
|
|
431
474
|
|
|
@@ -443,7 +486,7 @@ Worker's own accepted mutation publishes them as unavailable
|
|
|
443
486
|
| Endpoint condition | Problem |
|
|
444
487
|
|---|---|
|
|
445
488
|
| Cannot connect or complete discovery/catalog preparation at the negotiated revision | `502 server-unavailable`, retryable; names the server and its `type` without exposing credentials |
|
|
446
|
-
| HTTP 401 without configured credentials | Enabled `authorization-required`,
|
|
489
|
+
| HTTP 401 without configured credentials | Enabled `authorization-required`, its alias out of service ({§mcp-unavailable-runtime}), and no URL until the client begins sign-in |
|
|
447
490
|
| HTTP 401 with configured credentials | `502 server-authentication-failed`, non-retryable, with `upstreamStatus: 401`; credentials are not replaced by interactive OAuth |
|
|
448
491
|
| Sign-in discovery lacks validated authorization metadata or a usable client-registration method | `502 oauth-metadata-unavailable` or `oauth-registration-unavailable`, non-retryable; retain the definition and surface the setup boundary, not retryable downtime |
|
|
449
492
|
| The endpoint answers with a redirect | `502 server-redirected`, non-retryable ({§mcp-redirect-refused}) |
|
|
@@ -483,7 +526,8 @@ client is registered, or workspace lease retained merely for that challenge.
|
|
|
483
526
|
The client binds its callback before `oauth.begin`; absent an explicitly
|
|
484
527
|
configured redirect, it obtains an ephemeral loopback port (RFC 8252 §7.3).
|
|
485
528
|
The MCP SDK owns metadata discovery, registration, PKCE and token exchange.
|
|
486
|
-
An explicit OAuth definition may supply scope
|
|
529
|
+
An explicit OAuth definition may supply scope, client-registration metadata, an issuer and
|
|
530
|
+
authorization parameters ({§oauth-issuer});
|
|
487
531
|
its optional fixed redirect is respected, never silently rewritten. Bearer,
|
|
488
532
|
client-credentials and Authorization-header configurations cannot be replaced
|
|
489
533
|
by `oauth.begin`. Callback addresses and acquired credentials are session state,
|
|
@@ -504,29 +548,15 @@ sequenceDiagram
|
|
|
504
548
|
MCP-->>Model: active tools through normal publication
|
|
505
549
|
```
|
|
506
550
|
|
|
507
|
-
`oauth.begin` prepares an authorization attempt without enabling the alias or
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
the alias, not a capability snapshot
|
|
512
|
-
per `(workspace, alias)` holding the connection and one workspace residency
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
supersedes the prior one unless its grant is already accepted and awaiting
|
|
517
|
-
publication. An in-flight begin cannot attach after disable,
|
|
518
|
-
remove or definition replacement. `oauth.complete` accepts the complete callback URL so state, `code`, and
|
|
519
|
-
`iss` remain one parsing unit. Grant acceptance returns without waiting for
|
|
520
|
-
workspace quiescence; it is not a configuration mutation and cannot re-enable
|
|
521
|
-
an alias. The existing catalog-refresh path prepares and publishes the current
|
|
522
|
-
enabled definitions, then consumes the candidate and releases its lease.
|
|
523
|
-
The accepted result does not claim tool readiness: `list` reports the current
|
|
524
|
-
published snapshot, including any subsequent preparation failure. A callback
|
|
525
|
-
for a superseded attempt fails as invalid; a
|
|
526
|
-
committed attachment that no longer matches the pending definition fails
|
|
527
|
-
with a conflict instead of replaying a stale snapshot. A missing, expired,
|
|
528
|
-
mismatched, or replayed callback fails without exposing attacker-owned OAuth
|
|
529
|
-
error text.
|
|
551
|
+
`oauth.begin` prepares an authorization attempt without enabling the alias or waiting for workspace quiescence; `oauth.complete` accepts the complete callback URL so state, `code`, and `iss` remain one parsing unit.
|
|
552
|
+
|
|
553
|
+
| Fact | Rule |
|
|
554
|
+
| --- | --- |
|
|
555
|
+
| `oauth.begin` returns | `202` with an `authorization.url` when consent is required, `202` without that field when an accepted connection awaits publication, or `200` when tools are already active; these results name the alias, not a capability snapshot |
|
|
556
|
+
| The pending candidate | the adapter retains one per `(workspace, alias)` holding the connection and one workspace residency lease; an unchanged publication preserves that independent attempt, and a new challenge supersedes it ({§oauth-lifetime}) |
|
|
557
|
+
| Concurrent begins | concurrent begin requests for one alias are refused; a later begun attempt supersedes the prior one unless its grant is already accepted and awaiting publication; an in-flight begin cannot attach after disable, remove or definition replacement |
|
|
558
|
+
| Grant acceptance | returns without waiting for workspace quiescence; it is not a configuration mutation and cannot re-enable an alias; the existing catalog-refresh path prepares and publishes the current enabled definitions, then consumes the candidate and releases its lease; the accepted result does not claim tool readiness: `list` and a call to the alias report the current published snapshot, including any subsequent preparation failure, so a turn holding the workspace when the sign-in is accepted still sees the alias awaiting it, and the next turn sees its publication ({§module-workspace-quiescence}) |
|
|
559
|
+
| A bad callback | one for a superseded attempt fails as invalid; a committed attachment that no longer matches the pending definition fails with a conflict instead of replaying a stale snapshot; a missing, expired, mismatched, or replayed callback fails without exposing attacker-owned OAuth error text |
|
|
530
560
|
|
|
531
561
|
| Callback boundary | Result |
|
|
532
562
|
|---|---|
|
|
@@ -538,19 +568,40 @@ error text.
|
|
|
538
568
|
| Preparation failure published as unavailable | Preserve the preparation diagnostic; release the pending lease and close its unused connection. |
|
|
539
569
|
| Callback after publication | The attempt has been consumed; `404 oauth-not-pending`. |
|
|
540
570
|
|
|
571
|
+
§oauth-issuer **A configured issuer is where a sign-in starts and the one server it trusts.** An
|
|
572
|
+
interactive definition's `authorization.issuer` names the authorization server: discovery reads its
|
|
573
|
+
metadata (RFC 8414, then OpenID Connect Discovery) whether or not the MCP server publishes
|
|
574
|
+
protected-resource metadata, and the client registration, tokens and refresh are bound to it. With
|
|
575
|
+
none named, the SDK discovers the server from the resource's protected-resource metadata, falling
|
|
576
|
+
back to the resource's origin; where neither publishes metadata, a sign-in fails
|
|
577
|
+
`502 oauth-metadata-unavailable` and its recovery names the issuer field. `authorization.authorizationParams`
|
|
578
|
+
join every authorization request the definition makes, such as `access_type=offline` for a server
|
|
579
|
+
that issues a refresh token only when asked; the parameters the flow sets itself cannot be named
|
|
580
|
+
({§mcp-oauth}).
|
|
581
|
+
|
|
582
|
+
§oauth-begin-unchallenged **A sign-in starts where no connection is challenged.** A server may answer
|
|
583
|
+
its catalog to anyone and refuse only requests ({§mcp-request-unauthorized}), so the connection
|
|
584
|
+
`oauth.begin` prepares meets no challenge.
|
|
585
|
+
|
|
586
|
+
| The alias awaits a sign-in because | and begin's connection meets no challenge |
|
|
587
|
+
| --- | --- |
|
|
588
|
+
| a request was refused | begin starts the SDK's authorization on that connection and returns its URL as for a challenged one; a refusal of that start is a preparation failure ({§mcp-preparation-diagnostics}) and leaves no pending attempt |
|
|
589
|
+
| its connection was challenged | the connection is accepted as it is: `202` without a URL, publishing at the next boundary |
|
|
590
|
+
|
|
541
591
|
## §oauth-lifetime Interactive OAuth lifetime and reauthorization
|
|
542
592
|
|
|
543
|
-
Interactive OAuth state is
|
|
544
|
-
registration
|
|
545
|
-
|
|
546
|
-
OAuth-secret is written to SQLite; the durable
|
|
547
|
-
unexpanded definition ({§mcp-configuration}).
|
|
548
|
-
listener, authority-root resource, or
|
|
549
|
-
interactive client may receive its
|
|
550
|
-
pasted callback for remote/headless
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
593
|
+
Interactive OAuth state is process memory: an accepted sign-in's client
|
|
594
|
+
registration and access and refresh tokens are its alias's grant
|
|
595
|
+
({§oauth-grant}), and the PKCE verifier and pending state live only in the
|
|
596
|
+
pending candidate. Nothing OAuth-secret is written to SQLite; the durable
|
|
597
|
+
workspace state holds only the unexpanded definition ({§mcp-configuration}).
|
|
598
|
+
There is no daemon callback HTTP listener, authority-root resource, or
|
|
599
|
+
daemon-side browser side channel: an interactive client may receive its
|
|
600
|
+
configured loopback redirect, or accept a pasted callback for remote/headless
|
|
601
|
+
use. It returns the complete URL through `workspace.mcp.oauth.complete` so
|
|
602
|
+
`state`, `code`, and `iss` remain one parsing unit. Reauthorization after a
|
|
603
|
+
daemon restart is the intended journey, documented here rather than presented as
|
|
604
|
+
an accidental failure.
|
|
554
605
|
|
|
555
606
|
| Journey point | Behaviour |
|
|
556
607
|
|---|---|
|
|
@@ -558,11 +609,25 @@ as an accidental failure.
|
|
|
558
609
|
| Client disconnect | Does not touch the pending candidate; it can still be completed, or replaced by a fresh request. |
|
|
559
610
|
| Daemon restart during pending | The candidate is lost: nothing was durable, no attachment publishes, and `oauth.complete` answers `404 oauth-not-pending`. Start authorization again. |
|
|
560
611
|
| Daemon restart after authorization | The durable definition rehydrates but tokens are gone; a challenged attachment publishes `authorization-required` and enable returns `202`. A URL is present only when a callback is already configured. The operator reauthorizes. |
|
|
561
|
-
| Token expiry | An expired access token surfaces as one unauthorized response; the SDK re-acquires via `refresh_token` when one was issued
|
|
562
|
-
| Refresh | Happens only against the issuer bound during the original authorization; the refreshed token replaces the
|
|
563
|
-
| Workspace
|
|
612
|
+
| Token expiry | An expired access token surfaces as one unauthorized response; the SDK re-acquires via `refresh_token` when one was issued; otherwise the request fails and the alias awaits a sign-in ({§mcp-request-unauthorized}). |
|
|
613
|
+
| Refresh | Happens only against the issuer bound during the original authorization; the refreshed token replaces the grant's, for every later connection ({§oauth-grant}). |
|
|
614
|
+
| Workspace cooling | Closes the alias's connection; the grant outlives it, and the alias's next connection presents it ({§oauth-grant}). |
|
|
615
|
+
| Workspace disable/remove | Closes the attachment and clears its pending candidate; the grant stays for the alias's return with the definition it was accepted for ({§oauth-grant}). No durable secret deletion is needed because nothing secret is durable. |
|
|
564
616
|
| Server replacement | Publication discards any superseded pending connection and releases its residency, including replacement by a definition without OAuth. Completion of an unpublished configuration drift fails `409 oauth-target-conflict` instead of replaying a stale snapshot. |
|
|
565
|
-
| Cross-authorization protection | Candidates are keyed by `(workspace, alias)`; callback state, PKCE, and issuer are validated by the SDK against the attempt that created them, so no other workspace, alias, or attempt can complete this authorization. |
|
|
617
|
+
| Cross-authorization protection | Candidates and grants are keyed by `(workspace, alias)`; callback state, PKCE, and issuer are validated by the SDK against the attempt that created them, so no other workspace, alias, or attempt can complete this authorization. |
|
|
618
|
+
|
|
619
|
+
§oauth-grant **A grant outlives its connection.** An accepted sign-in is its alias's grant, held per
|
|
620
|
+
`(workspace, alias)` with the definition it was accepted for. Only a redirect the definition
|
|
621
|
+
configures still has a client waiting at it once the sign-in is accepted.
|
|
622
|
+
|
|
623
|
+
| When | The grant |
|
|
624
|
+
| --- | --- |
|
|
625
|
+
| A connection the module opens for the alias with that definition, after cooling, an explicit enable, or disable and enable | is presented, and refreshed as it lapses |
|
|
626
|
+
| An active alias's connection presents it | is listed in the alias's `detail.grant`: the `issuer` its tokens came from, the `scope` that authorization server stated, and whether it is `refreshable`; never a token or a time. An active alias presenting no grant lists none |
|
|
627
|
+
| A connection serves another definition, or another workspace | is never presented |
|
|
628
|
+
| Its server refuses it, at connection or on a request ({§mcp-request-unauthorized}) | starts a sign-in only at a redirect the definition configures; otherwise the alias awaits the sign-in a client begins ({§oauth-continuation}) |
|
|
629
|
+
| A sign-in for the alias is accepted | is replaced by the new one |
|
|
630
|
+
| Its authorization server rejects it, or the daemon stops | ends |
|
|
566
631
|
|
|
567
632
|
## §oauth-client-credentials Client-credentials grant adoption
|
|
568
633
|
|
|
@@ -638,19 +703,20 @@ boundaries; neither closes or replaces the committed connection.
|
|
|
638
703
|
| Aborted publication | Does not acknowledge the unpublished catalog. |
|
|
639
704
|
| Disable/remove, workspace cooling, or shutdown | Retires obsolete refresh timers and invalidations. |
|
|
640
705
|
|
|
641
|
-
MCP participates in core Functionality residency
|
|
642
|
-
Preparation reports the current server alias
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
Every tool call and Task retains the workspace
|
|
646
|
-
terminal result; an interactive OAuth candidate
|
|
647
|
-
replacement, cancellation, or module shutdown.
|
|
648
|
-
infrastructure, not residency owners: cooling
|
|
649
|
-
already running and cancels any timer not yet begun.
|
|
650
|
-
boundary, deactivation removes the workspace snapshot
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
706
|
+
MCP participates in core Functionality residency
|
|
707
|
+
({§module-workspace-residency}). Preparation reports the current server alias
|
|
708
|
+
through the coordinator's activity contract
|
|
709
|
+
({§functionality-preparation-visibility}); inspection never starts a connection
|
|
710
|
+
({§functionality-inspection}). Every tool call and Task retains the workspace
|
|
711
|
+
from executor entry through its terminal result; an interactive OAuth candidate
|
|
712
|
+
retains it until completion, replacement, cancellation, or module shutdown.
|
|
713
|
+
Catalog refresh timers are infrastructure, not residency owners: cooling
|
|
714
|
+
serializes behind a refresh already running and cancels any timer not yet begun.
|
|
715
|
+
At a lease-free quiescent boundary, deactivation removes the workspace snapshot
|
|
716
|
+
and closes all of its connections; an accepted sign-in outlives them
|
|
717
|
+
({§oauth-grant}). Core separately withdraws the executor/scheme publication
|
|
718
|
+
while preserving durable MCP state and generated reference entries for
|
|
719
|
+
transparent reactivation.
|
|
654
720
|
|
|
655
721
|
Add and enable prepare the candidate while the old snapshot remains
|
|
656
722
|
authoritative, then publish only at {§module-workspace-quiescence}. Disable and
|
|
@@ -682,6 +748,16 @@ failures are reported. Core's shutdown deadline remains the outer bound
|
|
|
682
748
|
|
|
683
749
|
## §mcp-host-composition Protocol-to-Plurnk composition
|
|
684
750
|
|
|
751
|
+
<!-- map -->
|
|
752
|
+
| Tag | Promise |
|
|
753
|
+
| --- | --- |
|
|
754
|
+
| {§mcp-input-deadline} | Client-input waits consume the remaining owning operation budget (`PLURNK_MCP_REQUEST_TIMEOUT`); each MRTR… |
|
|
755
|
+
| {§mcp-tool-replay} | A tool call is effectful unless host policy proves otherwise. |
|
|
756
|
+
| {§mcp-tool-problem-detail} | Tool-call Problems keep runtime, tool, and any bounded remote diagnostic as structured extensions. |
|
|
757
|
+
| {§mcp-request-unauthorized} | A request refused for want of a sign-in says so, and its alias then awaits one. |
|
|
758
|
+
| {§mcp-trailing-aside} | A tool call's body is one JSON object. |
|
|
759
|
+
<!-- /map -->
|
|
760
|
+
|
|
685
761
|
One `ServerConnection` owns negotiation, SDK caches, authorization partition,
|
|
686
762
|
subscriptions, active request controllers, MRTR rounds, and Tasks for one
|
|
687
763
|
workspace attachment. The host does not reproduce SDK protocol machinery.
|
|
@@ -729,6 +805,21 @@ Nontext parts and structured data are not interpreted as explanations. With no
|
|
|
729
805
|
text explanation the extension is absent; successful results do not acquire one.
|
|
730
806
|
The complete result and passive output remain unchanged ({§mcp-result-content}).
|
|
731
807
|
|
|
808
|
+
§mcp-request-unauthorized **A request refused for want of a sign-in says so, and its alias then awaits one.**
|
|
809
|
+
A server may answer its catalog to anyone and challenge only a request, so preparation can publish it
|
|
810
|
+
`active`. A later request — a tool call, a resource or prompt read, or a completion — is refused for
|
|
811
|
+
want of a sign-in when it is answered with HTTP 401, or challenged in a way the SDK cannot meet without
|
|
812
|
+
the operator's consent (an interactive grant it cannot refresh). The refusal depends on the definition:
|
|
813
|
+
|
|
814
|
+
| Definition | A request refused for want of a sign-in |
|
|
815
|
+
| --- | --- |
|
|
816
|
+
| URL-only, or interactive OAuth | fails `401 authorization-required`, non-retryable, and republishes the alias `authorization-required`, out of service ({§mcp-unavailable-runtime}), with no URL until the client begins sign-in ({§oauth-continuation}) |
|
|
817
|
+
| A bearer token, a client-credentials grant, or an Authorization header | fails as any failed request of its kind; no state changes and no sign-in is offered |
|
|
818
|
+
|
|
819
|
+
Only a fresh connection answers the awaited sign-in — an accepted sign-in, an explicit enable, a changed
|
|
820
|
+
definition, or the alias's return after disable, removal, cooling or a restart — and it publishes what
|
|
821
|
+
its preparation observes, presenting the alias's grant where its definition has one ({§oauth-grant}).
|
|
822
|
+
|
|
732
823
|
§mcp-trailing-aside A tool call's body is one JSON object. HTML comments after
|
|
733
824
|
that object are the writer's aside, not arguments: when the body does not parse
|
|
734
825
|
as written, trailing `<!-- … -->` comments are removed and the object is read.
|
|
@@ -891,9 +982,14 @@ servers are composition evidence only.
|
|
|
891
982
|
| `tool-not-enabled` | 404 | The MCP tool is not enabled. Recovery: Select a target documented under worker:///_plurnk/tools/*runtime*/. |
|
|
892
983
|
| `tool-reported-error` | 502 | The MCP tool reported an error (the tool call failed). |
|
|
893
984
|
| `invalid-tool-arguments` | 400 | MCP tool arguments must be one JSON object. Recovery: One JSON object per MCP tool call. |
|
|
985
|
+
| `resource-address-invalid` | 400 | The MCP resource address is invalid (a malformed encoded address); not retryable. |
|
|
986
|
+
| `resource-part-not-found` | 404 | The resource response does not contain this part. |
|
|
987
|
+
| `resource-read-failed` | 502 | MCP resource preparation failed; retryable, with the bounded upstream diagnostic. |
|
|
988
|
+
| `resource-find-failed` | 502 | MCP resource listing failed; retryable, with the bounded upstream diagnostic. |
|
|
894
989
|
| `oauth-client-credentials-failed` | 502 | MCP server '*name*' rejected the client-credentials grant. |
|
|
895
990
|
| `server-authentication-failed` | 502 | MCP server '*name*' rejected authentication (HTTP 401). |
|
|
896
|
-
| `
|
|
991
|
+
| `authorization-required` | 401 | MCP server '*name*' awaits the operator's sign-in; the request was not completed. |
|
|
992
|
+
| `oauth-metadata-unavailable` | 502 | MCP OAuth requires validated authorization-server metadata, and the server provided none. Recovery, when the definition names no issuer: Name the authorization server as the definition's authorization.issuer. |
|
|
897
993
|
| `oauth-registration-unavailable` | 502 | No client is registered with the authorization server: none is configured for it, it advertises no Dynamic Client Registration endpoint, and no client metadata document it accepts is configured. |
|
|
898
994
|
| `oauth-redirect-invalid` | 400 | The OAuth callback must use HTTPS or HTTP loopback with a usable port. |
|
|
899
995
|
| `oauth-configuration-conflict` | 409 | MCP server '*alias*' is not configured for this interactive OAuth callback. |
|
package/dist/McpExecutor.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseExecutor } from "@plurnk/plurnk-execs";
|
|
2
2
|
import type { ChannelDecl, Effect, ExecArgs, ExecResult, RuntimeAvailability, RuntimeDecl, RuntimeSummaryDecl, RuntimeToolRegistry } from "@plurnk/plurnk-execs";
|
|
3
|
-
import
|
|
3
|
+
import type ServerConnection from "./client.ts";
|
|
4
|
+
import { type ServerCatalog } from "./client.ts";
|
|
4
5
|
import type { ContentBlock } from "@modelcontextprotocol/client";
|
|
5
6
|
import type { ToolPolicy } from "./config.ts";
|
|
6
7
|
export declare const serverSummary: (name: string, catalog: ServerCatalog | undefined) => string;
|
|
@@ -14,6 +15,14 @@ export declare const toolResultBody: (result: ToolResultShape, runtime: string,
|
|
|
14
15
|
content: string;
|
|
15
16
|
mimetype: string;
|
|
16
17
|
}>;
|
|
18
|
+
export declare class UnavailableServerExecutor extends BaseExecutor {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(runtime: string, detail: string);
|
|
21
|
+
get channels(): Readonly<Record<string, ChannelDecl>>;
|
|
22
|
+
get publishedChannel(): string;
|
|
23
|
+
probe(): Promise<RuntimeAvailability>;
|
|
24
|
+
run(): Promise<ExecResult>;
|
|
25
|
+
}
|
|
17
26
|
export declare const runtimeDecl: (name: string, summary: RuntimeSummaryDecl, expandTools: boolean, instructions?: string) => RuntimeDecl;
|
|
18
27
|
export default class McpExecutor extends BaseExecutor {
|
|
19
28
|
#private;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpExecutor.d.ts","sourceRoot":"","sources":["../src/McpExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EAIf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EACR,WAAW,EACX,MAAM,EACN,QAAQ,EACR,UAAU,EAEV,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,gBAAgB,
|
|
1
|
+
{"version":3,"file":"McpExecutor.d.ts","sourceRoot":"","sources":["../src/McpExecutor.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EAIf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EACR,WAAW,EACX,MAAM,EACN,QAAQ,EACR,UAAU,EAEV,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,gBAAgB,MAAM,aAAa,CAAC;AAChD,OAAO,EAA4C,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,8BAA8B,CAAC;AAIjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAY9C,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,aAAa,GAAG,SAAS,KACnC,MAOF,CAAC;AAGF,eAAO,MAAM,oBAAoB,SACvB,MAAM,WACH,aAAa,GAAG,SAAS,KACnC,kBAKF,CAAC;AAMF,MAAM,MAAM,eAAe,GAAG;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,cAAc,WAAkB,eAAe,WAAW,MAAM,UAAU,QAAQ,CAAC,OAAO,CAAC,KAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAqBvJ,CAAC;AA2BF,qBAAa,yBAA0B,SAAQ,YAAY;;IAGvD,YAAY,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAG1C;IAED,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAEpD;IAED,IAAa,gBAAgB,IAAI,MAAM,CAEtC;IAEc,KAAK,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAEnD;IAEK,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC,CAE/B;CACJ;AAED,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,kBAAkB,eAAe,OAAO,iBAAiB,MAAM,KAAG,WAenH,CAAC;AAuBH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,YAAY;;IAoBjD,YACI,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAC5C,UAAU,EAAE,gBAAgB,EAC5B,eAAe,EAAE,MAAM,MAAM,IAAI,EACjC,MAAM,GAAE,OAAO,CAAC,UAAU,CAAM,EAMnC;IAED,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAOpD;IAEQ,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAK7C;IAED,IAAa,gBAAgB,IAAI,MAAM,CAEtC;IAyBD,YAAY,IAAI,mBAAmB,CAKlC;IAED,IAAI,OAAO,IAAI,aAAa,CAK3B;IAEc,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CASvE;IAEK,gBAAgB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsBzE;IAEK,GAAG,CAAC,EACN,OAAO,EACP,IAAI,EACJ,MAAM,EACN,MAAM,EACN,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,KAAK,GACR,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAuIhC;CACJ"}
|
package/dist/McpExecutor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseExecutor, ErrorDetail, Results, RuntimeInvocation, } from "@plurnk/plurnk-execs";
|
|
2
|
-
import { formatJsonDocument } from "@plurnk/plurnk-contracts";
|
|
3
|
-
import
|
|
2
|
+
import { isRecord, formatJsonDocument } from "@plurnk/plurnk-contracts";
|
|
3
|
+
import { AuthorizationRequiredError, awaitsSignIn } from "./client.js";
|
|
4
4
|
import { resourcePath } from "./McpResources.js";
|
|
5
5
|
import { HeaderDeclarationError } from "./protocolHeaders.js";
|
|
6
6
|
import ContentProjection from "./ContentProjection.js";
|
|
@@ -58,16 +58,38 @@ const pageReceipt = (args, tool, body) => {
|
|
|
58
58
|
const parsed = JSON.parse(body.content);
|
|
59
59
|
if (!Array.isArray(parsed))
|
|
60
60
|
return null;
|
|
61
|
-
const properties = (tool.inputSchema.properties
|
|
61
|
+
const properties = isRecord(tool.inputSchema.properties) ? tool.inputSchema.properties : {};
|
|
62
62
|
for (const name of PAGE_ARGUMENTS) {
|
|
63
63
|
const sent = args[name];
|
|
64
|
-
const
|
|
64
|
+
const property = properties[name];
|
|
65
|
+
const size = typeof sent === "number" ? sent : isRecord(property) ? property.default : undefined;
|
|
65
66
|
if (typeof size !== "number" || !Number.isInteger(size) || size <= 0)
|
|
66
67
|
continue;
|
|
67
68
|
return parsed.length === size ? { size, returned: parsed.length } : null;
|
|
68
69
|
}
|
|
69
70
|
return null;
|
|
70
71
|
};
|
|
72
|
+
// {§mcp-unavailable-runtime} An enabled server that cannot run keeps its alias: core refuses a call to it with the
|
|
73
|
+
// reason its availability states, before anything runs ({§exec-registry-resolves}), so this executor never runs.
|
|
74
|
+
export class UnavailableServerExecutor extends BaseExecutor {
|
|
75
|
+
#availability;
|
|
76
|
+
constructor(runtime, detail) {
|
|
77
|
+
super({ runtime, glyph: "🔌" });
|
|
78
|
+
this.#availability = { available: false, detail };
|
|
79
|
+
}
|
|
80
|
+
get channels() {
|
|
81
|
+
return { [CHANNEL]: { mimetype: "application/json" }, json: { mimetype: "application/json" } };
|
|
82
|
+
}
|
|
83
|
+
get publishedChannel() {
|
|
84
|
+
return CHANNEL;
|
|
85
|
+
}
|
|
86
|
+
async probe() {
|
|
87
|
+
return this.#availability;
|
|
88
|
+
}
|
|
89
|
+
async run() {
|
|
90
|
+
throw new Error(`MCP server '${this.runtime}' is unavailable and runs nothing: ${this.#availability.detail ?? ""}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
71
93
|
export const runtimeDecl = (name, summary, expandTools, instructions) => ({
|
|
72
94
|
name,
|
|
73
95
|
glyph: "🔌",
|
|
@@ -230,7 +252,7 @@ export default class McpExecutor extends BaseExecutor {
|
|
|
230
252
|
if (input.length > 0) {
|
|
231
253
|
try {
|
|
232
254
|
const parsed = McpExecutor.#parseArguments(input);
|
|
233
|
-
if (
|
|
255
|
+
if (!isRecord(parsed)) {
|
|
234
256
|
throw new TypeError("tool arguments must be an object");
|
|
235
257
|
}
|
|
236
258
|
args = parsed;
|
|
@@ -254,8 +276,8 @@ export default class McpExecutor extends BaseExecutor {
|
|
|
254
276
|
const result = await this.#connection.callTool(target, args, signal, (progress) => emit(progressNotice(runtime, target, progress)), interact, tool);
|
|
255
277
|
write("json", JSON.stringify(result, null, 2), "application/json");
|
|
256
278
|
setState("json", "closed");
|
|
257
|
-
const
|
|
258
|
-
write(CHANNEL,
|
|
279
|
+
const resultBody = await toolResultBody(result, runtime, entry);
|
|
280
|
+
write(CHANNEL, resultBody.content, resultBody.mimetype);
|
|
259
281
|
if (result.isError === true) {
|
|
260
282
|
const diagnostic = (result.content ?? [])
|
|
261
283
|
.flatMap((part) => part.type === "text" && part.text.trim().length > 0 ? [part.text] : [])
|
|
@@ -267,10 +289,14 @@ export default class McpExecutor extends BaseExecutor {
|
|
|
267
289
|
});
|
|
268
290
|
}
|
|
269
291
|
setState(CHANNEL, "closed");
|
|
270
|
-
const page = pageReceipt(args, tool,
|
|
292
|
+
const page = pageReceipt(args, tool, resultBody);
|
|
271
293
|
return { status: 200, ...(page === null ? {} : { page }) };
|
|
272
294
|
}
|
|
273
295
|
catch (error) {
|
|
296
|
+
if (error instanceof AuthorizationRequiredError) {
|
|
297
|
+
// {§mcp-request-unauthorized} — the alias now awaits a sign-in.
|
|
298
|
+
return fail("authorization-required", 401, awaitsSignIn(runtime), { tool: target, retryable: false });
|
|
299
|
+
}
|
|
274
300
|
if (error instanceof HeaderDeclarationError) {
|
|
275
301
|
// {§mcp-header-declarations} — refused before any request leg; the declaration is a field.
|
|
276
302
|
return fail("tool-header-invalid", 502, `MCP tool '${target}' declares an invalid x-mcp-header; the tool was not called.`, { tool: target, declaration: error.declaration, retryable: false });
|