@xenosystem/capability-mcp 0.1.31
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 +82 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +21 -0
- package/dist/bin.js.map +1 -0
- package/dist/capabilityClient.d.ts +81 -0
- package/dist/capabilityClient.d.ts.map +1 -0
- package/dist/capabilityClient.js +174 -0
- package/dist/capabilityClient.js.map +1 -0
- package/dist/catalogue.d.ts +63 -0
- package/dist/catalogue.d.ts.map +1 -0
- package/dist/catalogue.js +82 -0
- package/dist/catalogue.js.map +1 -0
- package/dist/descriptors.d.ts +39 -0
- package/dist/descriptors.d.ts.map +1 -0
- package/dist/descriptors.js +78 -0
- package/dist/descriptors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mcpServer.d.ts +57 -0
- package/dist/mcpServer.d.ts.map +1 -0
- package/dist/mcpServer.js +182 -0
- package/dist/mcpServer.js.map +1 -0
- package/package.json +39 -0
- package/scripts/smoke-live-app.mjs +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @xenosystem/capability-mcp
|
|
2
|
+
|
|
3
|
+
**The conversation inherits the tools of the applications you have open.**
|
|
4
|
+
|
|
5
|
+
Every XENO application already exposes what it can do over an authenticated loopback HTTP
|
|
6
|
+
surface (`XENO AGENT CAPABILITY - SPEC.md` §10.1). The agent conversation already accepts
|
|
7
|
+
extra tools — as MCP. Nothing translated between the two, so an agent running inside Canvas
|
|
8
|
+
could not draw a rectangle while Canvas exposed 73 operations three inches away.
|
|
9
|
+
|
|
10
|
+
This is that translation: one adapter, an MCP stdio server, that discovers every running XENO
|
|
11
|
+
application and presents its capabilities as tools named `<app>_<domain>_<verb>`.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Canvas / Pixel / Motion / Browser / Workflow / Engine / …
|
|
15
|
+
│ GET /capabilities · POST /capabilities/<name> (bearer token, loopback only)
|
|
16
|
+
▼
|
|
17
|
+
xeno-capability-mcp ← this package
|
|
18
|
+
│ MCP over stdio
|
|
19
|
+
▼
|
|
20
|
+
PluginMcpManagerService ← the existing seam, with its existing approval gate
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
the agent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Try it
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node dist/bin.js --list # what is running right now, and what is not
|
|
30
|
+
npm run smoke:live # spawn the real binary, drive a real app, assert it changed
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`--list` against a machine with Canvas open:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
73 capabilities from 1 running app(s): canvas
|
|
37
|
+
not available: motion — not running (stale descriptor, pid 188488)
|
|
38
|
+
…
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Five things that are deliberate
|
|
42
|
+
|
|
43
|
+
**1. Discovery is live, and re-run on every `tools/list`.** A descriptor in
|
|
44
|
+
`~/.xeno/capability-servers/` outlives the process that wrote it, so the *steady state* of
|
|
45
|
+
that directory is stale — measured 2026-09-08, all eight descriptors named dead pids. An
|
|
46
|
+
adapter that trusted the file would offer an agent operations that cannot run.
|
|
47
|
+
|
|
48
|
+
**2. A closed application is ABSENT, not FAILING.** `canvas is not available: not running.
|
|
49
|
+
Ask the user to open canvas, then retry` and `no such tool` lead to opposite next actions, and
|
|
50
|
+
an agent told only the second concludes the capability does not exist.
|
|
51
|
+
|
|
52
|
+
**3. The application's own refusal stays a refusal.** The §8 envelope is passed through
|
|
53
|
+
uninterpreted and `isError` is derived from it. Returning `{ ok: false }` as a successful tool
|
|
54
|
+
result is the fabricated-success shape that let `xeno-workflow` ship 41 nodes reporting
|
|
55
|
+
success while doing nothing.
|
|
56
|
+
|
|
57
|
+
**4. The token never leaves this process.** Descriptors are `0600` and carry a per-launch
|
|
58
|
+
bearer token. It goes on the wire and into no tool result — asserted by a test.
|
|
59
|
+
|
|
60
|
+
**5. `inputSchema` is a COMPLETE JSON Schema**, per the MCP spec. `@xenosystem/agent-sdk` used
|
|
61
|
+
to read it as a bare properties map, so every MCP tool in every XENO product reached the model
|
|
62
|
+
with parameters named `type`, `properties` and `required` and the real ones invisible. Fixed
|
|
63
|
+
in that repo's `src/mcp/tool-adapter.ts`; do not "fix" this end to match an old client.
|
|
64
|
+
|
|
65
|
+
## The envelope divergence this absorbs
|
|
66
|
+
|
|
67
|
+
Measured across the eight shipped capability servers on 2026-09-08:
|
|
68
|
+
|
|
69
|
+
| application | `GET /capabilities` body | schema field |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| canvas, motion, pixel, workflow, sound, docs | `{ capabilities: [...] }` | `argsSchema` |
|
|
72
|
+
| engine | `{ ok: true, capabilities: [...] }` | `inputSchema` |
|
|
73
|
+
|
|
74
|
+
Eight applications each growing their own MCP endpoint would be eight places to absorb that,
|
|
75
|
+
drifting independently. Here it is one function — and any future application that merely has a
|
|
76
|
+
capability server is covered without writing anything.
|
|
77
|
+
|
|
78
|
+
## Ports
|
|
79
|
+
|
|
80
|
+
Read the port from the descriptor, never from `XENO PORT ALLOCATIONS.md`. That document
|
|
81
|
+
records a *preference*; an application that cannot get its preferred port binds another and
|
|
82
|
+
publishes what it actually obtained. Canvas has been observed on 7333, 7395 and 30611.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The stdio entry point `PluginMcpManagerService` spawns.
|
|
4
|
+
*
|
|
5
|
+
* `--list` prints what is reachable right now and exits, because the first question anyone
|
|
6
|
+
* asks of this thing is "is anything running", and answering it should not require speaking
|
|
7
|
+
* JSON-RPC by hand.
|
|
8
|
+
*/
|
|
9
|
+
import { discover } from './catalogue.js';
|
|
10
|
+
import { serve } from './mcpServer.js';
|
|
11
|
+
const argv = process.argv.slice(2);
|
|
12
|
+
if (argv.includes('--list')) {
|
|
13
|
+
const catalogue = await discover();
|
|
14
|
+
const apps = [...new Set(catalogue.entries.map((entry) => entry.app))];
|
|
15
|
+
process.stdout.write(`${catalogue.entries.length} capabilities from ${apps.length} running app(s): ${apps.join(', ') || 'none'}\n`);
|
|
16
|
+
for (const { app, reason } of catalogue.unreachable)
|
|
17
|
+
process.stdout.write(` not available: ${app} — ${reason}\n`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
await serve();
|
|
21
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAElC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,MAAM,QAAQ,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,sBAAsB,IAAI,CAAC,MAAM,oBAAoB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,CAAA;IACnI,KAAK,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,WAAW;QAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,MAAM,MAAM,IAAI,CAAC,CAAA;IAClH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC;AAED,MAAM,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talking to one XENO application's capability server.
|
|
3
|
+
*
|
|
4
|
+
* `GET /capabilities` and `POST /capabilities/<name>` are the whole surface
|
|
5
|
+
* (`XENO AGENT CAPABILITY - SPEC.md` §10.1), and every app bearer-authenticates with the
|
|
6
|
+
* per-launch token from its descriptor.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **The eight shipped servers do NOT agree on the response envelope, and that divergence
|
|
9
|
+
* is the reason ONE adapter is the right shape.** Measured 2026-09-08:
|
|
10
|
+
*
|
|
11
|
+
* | app | `GET /capabilities` body | schema field |
|
|
12
|
+
* |---|---|---|
|
|
13
|
+
* | canvas, motion, pixel, workflow, sound, docs | `{ capabilities: [...] }` | `argsSchema` |
|
|
14
|
+
* | engine | `{ ok: true, capabilities: [...] }` | `inputSchema` |
|
|
15
|
+
*
|
|
16
|
+
* Eight apps each growing their own MCP endpoint would be eight places to absorb that; here
|
|
17
|
+
* it is one function, and every future app that merely has a capability server is covered
|
|
18
|
+
* without writing anything.
|
|
19
|
+
*
|
|
20
|
+
* This deliberately does NOT reconcile the divergence upstream: `XENO AGENT CAPABILITY -
|
|
21
|
+
* SPEC.md` §15 makes *exposing* the product's job and *driving* somebody else's, and the
|
|
22
|
+
* products are shipping. Normalising here is the driving half doing its job.
|
|
23
|
+
*/
|
|
24
|
+
import type { CapabilityDescriptor } from './descriptors.js';
|
|
25
|
+
/** One operation an application offers, normalised across the envelope divergence above. */
|
|
26
|
+
export interface Capability {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
/** A JSON Schema for the arguments. `{ type: 'object', properties: {} }` when absent. */
|
|
30
|
+
schema: {
|
|
31
|
+
type: 'object';
|
|
32
|
+
properties: Record<string, unknown>;
|
|
33
|
+
required?: string[] | undefined;
|
|
34
|
+
};
|
|
35
|
+
/** `safe` | `undoable` | `irreversible` when the app declares it. */
|
|
36
|
+
reversibility?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface CapabilityClientOptions {
|
|
39
|
+
fetchImpl?: typeof fetch | undefined;
|
|
40
|
+
timeoutMs?: number | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pull the capability array out of whatever envelope this app answered with.
|
|
44
|
+
*
|
|
45
|
+
* Returns `undefined` rather than `[]` when nothing recognisable is there. That difference
|
|
46
|
+
* is load-bearing: an empty array says "this app has no capabilities" and an unreadable body
|
|
47
|
+
* says "this is not one of our servers" — and the second is how a descriptor whose port has
|
|
48
|
+
* been recycled by an unrelated process gets refused instead of advertised.
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractCapabilities(body: unknown): unknown[] | undefined;
|
|
51
|
+
/** Normalise one descriptor entry. `argsSchema` and `inputSchema` are both in the wild. */
|
|
52
|
+
export declare function normalizeCapability(entry: unknown): Capability | undefined;
|
|
53
|
+
export declare class CapabilityUnavailable extends Error {
|
|
54
|
+
readonly app: string;
|
|
55
|
+
constructor(app: string, message: string);
|
|
56
|
+
}
|
|
57
|
+
/** A thin, tolerant client for one app's capability server. */
|
|
58
|
+
export declare class CapabilityClient {
|
|
59
|
+
readonly descriptor: CapabilityDescriptor;
|
|
60
|
+
private readonly fetchImpl;
|
|
61
|
+
private readonly timeoutMs;
|
|
62
|
+
constructor(descriptor: CapabilityDescriptor, options?: CapabilityClientOptions);
|
|
63
|
+
private request;
|
|
64
|
+
/**
|
|
65
|
+
* What can this app do right now?
|
|
66
|
+
*
|
|
67
|
+
* This IS the liveness check. A pid says a process exists; only a successful, correctly
|
|
68
|
+
* shaped answer says this app is up, is ours, and honours the token we hold.
|
|
69
|
+
*/
|
|
70
|
+
listCapabilities(): Promise<Capability[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Run one capability and return its raw §8 envelope.
|
|
73
|
+
*
|
|
74
|
+
* The envelope is returned UNINTERPRETED. It already distinguishes success from failure
|
|
75
|
+
* with a code the agent can act on, and re-deciding that here would put two opinions in
|
|
76
|
+
* the path that can disagree — the shape that produced `{ success: true }` with nothing
|
|
77
|
+
* done in xeno-workflow's 41 fabricated nodes.
|
|
78
|
+
*/
|
|
79
|
+
invoke(name: string, args: Record<string, unknown>): Promise<unknown>;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=capabilityClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilityClient.d.ts","sourceRoot":"","sources":["../src/capabilityClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAChG,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B;AAUD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,SAAS,CAUxE;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAkB1E;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAE5C,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM;CAKlB;AAED,+DAA+D;AAC/D,qBAAa,gBAAgB;IAKzB,QAAQ,CAAC,UAAU,EAAE,oBAAoB;IAJ3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;gBAGvB,UAAU,EAAE,oBAAoB,EACzC,OAAO,GAAE,uBAA4B;YAMzB,OAAO;IAoBrB;;;;;OAKG;IACG,gBAAgB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAkC/C;;;;;;;OAOG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;CA2B5E"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talking to one XENO application's capability server.
|
|
3
|
+
*
|
|
4
|
+
* `GET /capabilities` and `POST /capabilities/<name>` are the whole surface
|
|
5
|
+
* (`XENO AGENT CAPABILITY - SPEC.md` §10.1), and every app bearer-authenticates with the
|
|
6
|
+
* per-launch token from its descriptor.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **The eight shipped servers do NOT agree on the response envelope, and that divergence
|
|
9
|
+
* is the reason ONE adapter is the right shape.** Measured 2026-09-08:
|
|
10
|
+
*
|
|
11
|
+
* | app | `GET /capabilities` body | schema field |
|
|
12
|
+
* |---|---|---|
|
|
13
|
+
* | canvas, motion, pixel, workflow, sound, docs | `{ capabilities: [...] }` | `argsSchema` |
|
|
14
|
+
* | engine | `{ ok: true, capabilities: [...] }` | `inputSchema` |
|
|
15
|
+
*
|
|
16
|
+
* Eight apps each growing their own MCP endpoint would be eight places to absorb that; here
|
|
17
|
+
* it is one function, and every future app that merely has a capability server is covered
|
|
18
|
+
* without writing anything.
|
|
19
|
+
*
|
|
20
|
+
* This deliberately does NOT reconcile the divergence upstream: `XENO AGENT CAPABILITY -
|
|
21
|
+
* SPEC.md` §15 makes *exposing* the product's job and *driving* somebody else's, and the
|
|
22
|
+
* products are shipping. Normalising here is the driving half doing its job.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
25
|
+
function asObject(value) {
|
|
26
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
27
|
+
? value
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pull the capability array out of whatever envelope this app answered with.
|
|
32
|
+
*
|
|
33
|
+
* Returns `undefined` rather than `[]` when nothing recognisable is there. That difference
|
|
34
|
+
* is load-bearing: an empty array says "this app has no capabilities" and an unreadable body
|
|
35
|
+
* says "this is not one of our servers" — and the second is how a descriptor whose port has
|
|
36
|
+
* been recycled by an unrelated process gets refused instead of advertised.
|
|
37
|
+
*/
|
|
38
|
+
export function extractCapabilities(body) {
|
|
39
|
+
const root = asObject(body);
|
|
40
|
+
if (!root)
|
|
41
|
+
return Array.isArray(body) ? body : undefined;
|
|
42
|
+
if (Array.isArray(root['capabilities']))
|
|
43
|
+
return root['capabilities'];
|
|
44
|
+
// A manifest object carrying the list one level down.
|
|
45
|
+
const nested = asObject(root['capabilities']);
|
|
46
|
+
if (nested && Array.isArray(nested['capabilities']))
|
|
47
|
+
return nested['capabilities'];
|
|
48
|
+
const data = asObject(root['data']);
|
|
49
|
+
if (data && Array.isArray(data['capabilities']))
|
|
50
|
+
return data['capabilities'];
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Normalise one descriptor entry. `argsSchema` and `inputSchema` are both in the wild. */
|
|
54
|
+
export function normalizeCapability(entry) {
|
|
55
|
+
const record = asObject(entry);
|
|
56
|
+
const name = record?.['name'];
|
|
57
|
+
if (typeof name !== 'string' || !name)
|
|
58
|
+
return undefined;
|
|
59
|
+
const rawSchema = asObject(record?.['argsSchema']) ?? asObject(record?.['inputSchema']) ?? asObject(record?.['schema']);
|
|
60
|
+
const properties = asObject(rawSchema?.['properties']) ?? {};
|
|
61
|
+
const rawRequired = rawSchema?.['required'];
|
|
62
|
+
const required = Array.isArray(rawRequired)
|
|
63
|
+
? rawRequired.filter((value) => typeof value === 'string')
|
|
64
|
+
: undefined;
|
|
65
|
+
return {
|
|
66
|
+
name,
|
|
67
|
+
description: typeof record?.['description'] === 'string' ? record['description'] : name,
|
|
68
|
+
schema: { type: 'object', properties, ...(required && required.length ? { required } : {}) },
|
|
69
|
+
reversibility: typeof record?.['reversibility'] === 'string' ? record['reversibility'] : undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export class CapabilityUnavailable extends Error {
|
|
73
|
+
app;
|
|
74
|
+
constructor(app, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.app = app;
|
|
77
|
+
this.name = 'CapabilityUnavailable';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** A thin, tolerant client for one app's capability server. */
|
|
81
|
+
export class CapabilityClient {
|
|
82
|
+
descriptor;
|
|
83
|
+
fetchImpl;
|
|
84
|
+
timeoutMs;
|
|
85
|
+
constructor(descriptor, options = {}) {
|
|
86
|
+
this.descriptor = descriptor;
|
|
87
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
88
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
89
|
+
}
|
|
90
|
+
async request(path, init) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
return await this.fetchImpl(`${this.descriptor.url}${path}`, {
|
|
95
|
+
...init,
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
headers: {
|
|
98
|
+
...init?.headers,
|
|
99
|
+
// The token stays in this process. It is read from a 0600 descriptor and put on
|
|
100
|
+
// the wire; it is never part of a tool result, so it cannot reach a renderer or a
|
|
101
|
+
// model transcript.
|
|
102
|
+
authorization: `Bearer ${this.descriptor.token}`,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* What can this app do right now?
|
|
112
|
+
*
|
|
113
|
+
* This IS the liveness check. A pid says a process exists; only a successful, correctly
|
|
114
|
+
* shaped answer says this app is up, is ours, and honours the token we hold.
|
|
115
|
+
*/
|
|
116
|
+
async listCapabilities() {
|
|
117
|
+
let response;
|
|
118
|
+
try {
|
|
119
|
+
response = await this.request('/capabilities');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} is not answering on ${this.descriptor.url}: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered ${response.status} for GET /capabilities.`);
|
|
126
|
+
}
|
|
127
|
+
let body;
|
|
128
|
+
try {
|
|
129
|
+
body = await response.json();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered with non-JSON.`);
|
|
133
|
+
}
|
|
134
|
+
const entries = extractCapabilities(body);
|
|
135
|
+
if (!entries) {
|
|
136
|
+
throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered a body with no capability list — the port may belong to something else now.`);
|
|
137
|
+
}
|
|
138
|
+
return entries
|
|
139
|
+
.map((entry) => normalizeCapability(entry))
|
|
140
|
+
.filter((capability) => capability !== undefined);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run one capability and return its raw §8 envelope.
|
|
144
|
+
*
|
|
145
|
+
* The envelope is returned UNINTERPRETED. It already distinguishes success from failure
|
|
146
|
+
* with a code the agent can act on, and re-deciding that here would put two opinions in
|
|
147
|
+
* the path that can disagree — the shape that produced `{ success: true }` with nothing
|
|
148
|
+
* done in xeno-workflow's 41 fabricated nodes.
|
|
149
|
+
*/
|
|
150
|
+
async invoke(name, args) {
|
|
151
|
+
let response;
|
|
152
|
+
try {
|
|
153
|
+
response = await this.request(`/capabilities/${encodeURIComponent(name)}`, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({ args }),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} is not answering: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
const text = await response.text();
|
|
163
|
+
if (!response.ok && !text) {
|
|
164
|
+
throw new CapabilityUnavailable(this.descriptor.app, `${this.descriptor.app} answered ${response.status}.`);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(text);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return { ok: false, error: { code: 'internal', message: text.slice(0, 2000) } };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=capabilityClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilityClient.js","sourceRoot":"","sources":["../src/capabilityClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every capability of every RUNNING XENO application, as one tool list.
|
|
3
|
+
*
|
|
4
|
+
* Discovery is deliberately re-run on every `tools/list`. An app the user opens after the
|
|
5
|
+
* conversation started must appear, and an app they close must disappear — a catalogue
|
|
6
|
+
* cached at startup is exactly how an agent ends up offering operations that cannot run.
|
|
7
|
+
*/
|
|
8
|
+
import { type Capability } from './capabilityClient.js';
|
|
9
|
+
import { type CapabilityDescriptor } from './descriptors.js';
|
|
10
|
+
export interface CatalogueEntry {
|
|
11
|
+
app: string;
|
|
12
|
+
capability: Capability;
|
|
13
|
+
/** The MCP tool name. App-qualified, and safe for every MCP client. */
|
|
14
|
+
toolName: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Catalogue {
|
|
17
|
+
entries: CatalogueEntry[];
|
|
18
|
+
/**
|
|
19
|
+
* Apps found on disk that could not be reached, with the reason.
|
|
20
|
+
*
|
|
21
|
+
* Kept rather than discarded: "Canvas is not running" and "Canvas has no such capability"
|
|
22
|
+
* are different instructions for an agent, and only the first is recoverable by asking the
|
|
23
|
+
* user to open the app.
|
|
24
|
+
*/
|
|
25
|
+
unreachable: {
|
|
26
|
+
app: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* MCP tool name for one capability.
|
|
32
|
+
*
|
|
33
|
+
* The name is app-qualified so two apps cannot collide and so the agent can say WHICH app it
|
|
34
|
+
* is acting on. Capability names are already qualified in every shipped app
|
|
35
|
+
* (`canvas.document.create`), so the prefix is added only when it is missing — otherwise the
|
|
36
|
+
* tool would read `canvas_canvas_document_create`.
|
|
37
|
+
*
|
|
38
|
+
* Dots become underscores. The consuming SDK's sanitiser permits dots
|
|
39
|
+
* (`/[^A-Za-z0-9_.-]+/`), but Anthropic's tool-name rule is `^[a-zA-Z0-9_-]{1,64}$` and this
|
|
40
|
+
* server is meant to work with any MCP client, so the conservative alphabet is the one that
|
|
41
|
+
* cannot be wrong somewhere else.
|
|
42
|
+
*/
|
|
43
|
+
export declare function toolNameFor(app: string, capabilityName: string): string;
|
|
44
|
+
export interface DiscoverOptions {
|
|
45
|
+
dir?: string | undefined;
|
|
46
|
+
fetchImpl?: typeof fetch | undefined;
|
|
47
|
+
timeoutMs?: number | undefined;
|
|
48
|
+
/** Injected so the liveness rule can be tested without spawning processes. */
|
|
49
|
+
isAlive?: ((pid: number | undefined) => boolean) | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Descriptors whose publishing process still exists. Cheap, and never the whole answer. */
|
|
52
|
+
export declare function candidateDescriptors(options?: DiscoverOptions): {
|
|
53
|
+
candidates: CapabilityDescriptor[];
|
|
54
|
+
dead: {
|
|
55
|
+
app: string;
|
|
56
|
+
reason: string;
|
|
57
|
+
}[];
|
|
58
|
+
};
|
|
59
|
+
/** Build the live catalogue. Apps are probed in parallel; one being down never blocks another. */
|
|
60
|
+
export declare function discover(options?: DiscoverOptions): Promise<Catalogue>;
|
|
61
|
+
/** The description a model sees. It has to carry the app and the risk, not just the verb. */
|
|
62
|
+
export declare function describeEntry(entry: CatalogueEntry): string;
|
|
63
|
+
//# sourceMappingURL=catalogue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogue.d.ts","sourceRoot":"","sources":["../src/catalogue.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA2C,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAChG,OAAO,EAAmC,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE7F,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,UAAU,CAAA;IACtB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB;;;;;;OAMG;IACH,WAAW,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,SAAS,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,GAAG,SAAS,CAAA;CAC7D;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,eAAoB,GAAG;IACnE,UAAU,EAAE,oBAAoB,EAAE,CAAA;IAClC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxC,CASA;AAED,kGAAkG;AAClG,wBAAsB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,SAAS,CAAC,CAmChF;AAED,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAM3D"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every capability of every RUNNING XENO application, as one tool list.
|
|
3
|
+
*
|
|
4
|
+
* Discovery is deliberately re-run on every `tools/list`. An app the user opens after the
|
|
5
|
+
* conversation started must appear, and an app they close must disappear — a catalogue
|
|
6
|
+
* cached at startup is exactly how an agent ends up offering operations that cannot run.
|
|
7
|
+
*/
|
|
8
|
+
import { CapabilityClient, CapabilityUnavailable } from './capabilityClient.js';
|
|
9
|
+
import { processIsAlive, readDescriptors } from './descriptors.js';
|
|
10
|
+
/**
|
|
11
|
+
* MCP tool name for one capability.
|
|
12
|
+
*
|
|
13
|
+
* The name is app-qualified so two apps cannot collide and so the agent can say WHICH app it
|
|
14
|
+
* is acting on. Capability names are already qualified in every shipped app
|
|
15
|
+
* (`canvas.document.create`), so the prefix is added only when it is missing — otherwise the
|
|
16
|
+
* tool would read `canvas_canvas_document_create`.
|
|
17
|
+
*
|
|
18
|
+
* Dots become underscores. The consuming SDK's sanitiser permits dots
|
|
19
|
+
* (`/[^A-Za-z0-9_.-]+/`), but Anthropic's tool-name rule is `^[a-zA-Z0-9_-]{1,64}$` and this
|
|
20
|
+
* server is meant to work with any MCP client, so the conservative alphabet is the one that
|
|
21
|
+
* cannot be wrong somewhere else.
|
|
22
|
+
*/
|
|
23
|
+
export function toolNameFor(app, capabilityName) {
|
|
24
|
+
const qualified = capabilityName.startsWith(`${app}.`) ? capabilityName : `${app}.${capabilityName}`;
|
|
25
|
+
return qualified.replace(/[^A-Za-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 64);
|
|
26
|
+
}
|
|
27
|
+
/** Descriptors whose publishing process still exists. Cheap, and never the whole answer. */
|
|
28
|
+
export function candidateDescriptors(options = {}) {
|
|
29
|
+
const isAlive = options.isAlive ?? processIsAlive;
|
|
30
|
+
const candidates = [];
|
|
31
|
+
const dead = [];
|
|
32
|
+
for (const descriptor of readDescriptors(options.dir)) {
|
|
33
|
+
if (isAlive(descriptor.pid))
|
|
34
|
+
candidates.push(descriptor);
|
|
35
|
+
else
|
|
36
|
+
dead.push({ app: descriptor.app, reason: `not running (stale descriptor, pid ${descriptor.pid})` });
|
|
37
|
+
}
|
|
38
|
+
return { candidates, dead };
|
|
39
|
+
}
|
|
40
|
+
/** Build the live catalogue. Apps are probed in parallel; one being down never blocks another. */
|
|
41
|
+
export async function discover(options = {}) {
|
|
42
|
+
const { candidates, dead } = candidateDescriptors(options);
|
|
43
|
+
const entries = [];
|
|
44
|
+
const unreachable = [...dead];
|
|
45
|
+
const probes = candidates.map(async (descriptor) => {
|
|
46
|
+
const client = new CapabilityClient(descriptor, {
|
|
47
|
+
fetchImpl: options.fetchImpl,
|
|
48
|
+
timeoutMs: options.timeoutMs,
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const capabilities = await client.listCapabilities();
|
|
52
|
+
return { descriptor, capabilities };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const reason = error instanceof CapabilityUnavailable ? error.message : `unreachable: ${error.message}`;
|
|
56
|
+
unreachable.push({ app: descriptor.app, reason });
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
for (const probe of await Promise.all(probes)) {
|
|
61
|
+
if (!probe)
|
|
62
|
+
continue;
|
|
63
|
+
for (const capability of probe.capabilities) {
|
|
64
|
+
entries.push({
|
|
65
|
+
app: probe.descriptor.app,
|
|
66
|
+
capability,
|
|
67
|
+
toolName: toolNameFor(probe.descriptor.app, capability.name),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
entries.sort((a, b) => a.toolName.localeCompare(b.toolName));
|
|
72
|
+
unreachable.sort((a, b) => a.app.localeCompare(b.app));
|
|
73
|
+
return { entries, unreachable };
|
|
74
|
+
}
|
|
75
|
+
/** The description a model sees. It has to carry the app and the risk, not just the verb. */
|
|
76
|
+
export function describeEntry(entry) {
|
|
77
|
+
const risk = entry.capability.reversibility && entry.capability.reversibility !== 'safe'
|
|
78
|
+
? ` [${entry.capability.reversibility}]`
|
|
79
|
+
: '';
|
|
80
|
+
return `[${entry.app}]${risk} ${entry.capability.description}`;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=catalogue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogue.js","sourceRoot":"","sources":["../src/catalogue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the XENO applications that are running right now.
|
|
3
|
+
*
|
|
4
|
+
* Every XENO app that opens a capability server publishes `{ url, token, pid }` to
|
|
5
|
+
* `~/.xeno/capability-servers/<app>.json` (`XENO AGENT CAPABILITY - SPEC.md` §10.1). That
|
|
6
|
+
* directory is discovery, and it is the ONLY trustworthy source for where an app is: the
|
|
7
|
+
* documented port block in `XENO PORT ALLOCATIONS.md` is a *preference*, and an app that
|
|
8
|
+
* loses its preferred port binds another and publishes the one it actually got. Measured
|
|
9
|
+
* 2026-09-08: Canvas's descriptor said 7395 against a documented 7333, and five of eight
|
|
10
|
+
* apps sat outside the documented block entirely.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **A descriptor is not a running server.** The file outlives the process that wrote it,
|
|
13
|
+
* so the STEADY STATE of that directory is stale, not live — on 2026-09-08 all eight
|
|
14
|
+
* descriptors named dead pids. Anything that reads a descriptor and concludes the app is up
|
|
15
|
+
* will offer an agent tools that cannot run, and the agent will report the app broken.
|
|
16
|
+
* {@link liveDescriptors} exists so that mistake is not available.
|
|
17
|
+
*/
|
|
18
|
+
/** Where every XENO app publishes itself. */
|
|
19
|
+
export declare const CAPABILITY_SERVER_DIR: string;
|
|
20
|
+
export interface CapabilityDescriptor {
|
|
21
|
+
/** The app's slug, taken from the FILE NAME — `canvas.json` is `canvas`. */
|
|
22
|
+
app: string;
|
|
23
|
+
url: string;
|
|
24
|
+
token: string;
|
|
25
|
+
pid?: number | undefined;
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
/** Read every descriptor on disk. Says nothing about whether any of them is alive. */
|
|
29
|
+
export declare function readDescriptors(dir?: string): CapabilityDescriptor[];
|
|
30
|
+
/**
|
|
31
|
+
* Is the process that published this descriptor still alive?
|
|
32
|
+
*
|
|
33
|
+
* `kill(pid, 0)` sends no signal; it asks whether the pid can be signalled. **`EPERM` means
|
|
34
|
+
* the process EXISTS** and belongs to someone else, so treating any throw as "dead" would
|
|
35
|
+
* report a running app as gone. A descriptor with no pid is not evidence of death either —
|
|
36
|
+
* it is absence of evidence, and the HTTP probe decides.
|
|
37
|
+
*/
|
|
38
|
+
export declare function processIsAlive(pid: number | undefined): boolean;
|
|
39
|
+
//# sourceMappingURL=descriptors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptors.d.ts","sourceRoot":"","sources":["../src/descriptors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,6CAA6C;AAC7C,eAAO,MAAM,qBAAqB,QAAiD,CAAA;AAEnF,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAA8B,GAAG,oBAAoB,EAAE,CA8B3F;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAQ/D"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the XENO applications that are running right now.
|
|
3
|
+
*
|
|
4
|
+
* Every XENO app that opens a capability server publishes `{ url, token, pid }` to
|
|
5
|
+
* `~/.xeno/capability-servers/<app>.json` (`XENO AGENT CAPABILITY - SPEC.md` §10.1). That
|
|
6
|
+
* directory is discovery, and it is the ONLY trustworthy source for where an app is: the
|
|
7
|
+
* documented port block in `XENO PORT ALLOCATIONS.md` is a *preference*, and an app that
|
|
8
|
+
* loses its preferred port binds another and publishes the one it actually got. Measured
|
|
9
|
+
* 2026-09-08: Canvas's descriptor said 7395 against a documented 7333, and five of eight
|
|
10
|
+
* apps sat outside the documented block entirely.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **A descriptor is not a running server.** The file outlives the process that wrote it,
|
|
13
|
+
* so the STEADY STATE of that directory is stale, not live — on 2026-09-08 all eight
|
|
14
|
+
* descriptors named dead pids. Anything that reads a descriptor and concludes the app is up
|
|
15
|
+
* will offer an agent tools that cannot run, and the agent will report the app broken.
|
|
16
|
+
* {@link liveDescriptors} exists so that mistake is not available.
|
|
17
|
+
*/
|
|
18
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
/** Where every XENO app publishes itself. */
|
|
22
|
+
export const CAPABILITY_SERVER_DIR = join(homedir(), '.xeno', 'capability-servers');
|
|
23
|
+
/** Read every descriptor on disk. Says nothing about whether any of them is alive. */
|
|
24
|
+
export function readDescriptors(dir = CAPABILITY_SERVER_DIR) {
|
|
25
|
+
let entries;
|
|
26
|
+
try {
|
|
27
|
+
entries = readdirSync(dir);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// No directory means no XENO app has ever opened a capability port here. That is a
|
|
31
|
+
// legitimate empty answer, not a failure to report.
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const descriptors = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.endsWith('.json'))
|
|
37
|
+
continue;
|
|
38
|
+
const path = join(dir, entry);
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
if (typeof parsed.url !== 'string' || typeof parsed.token !== 'string')
|
|
42
|
+
continue;
|
|
43
|
+
descriptors.push({
|
|
44
|
+
app: entry.slice(0, -'.json'.length),
|
|
45
|
+
url: parsed.url.replace(/\/+$/, ''),
|
|
46
|
+
token: parsed.token,
|
|
47
|
+
pid: typeof parsed.pid === 'number' ? parsed.pid : undefined,
|
|
48
|
+
path,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A half-written or corrupt descriptor is one app being unreachable, never a reason
|
|
53
|
+
// to hide the others.
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return descriptors.sort((a, b) => a.app.localeCompare(b.app));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Is the process that published this descriptor still alive?
|
|
61
|
+
*
|
|
62
|
+
* `kill(pid, 0)` sends no signal; it asks whether the pid can be signalled. **`EPERM` means
|
|
63
|
+
* the process EXISTS** and belongs to someone else, so treating any throw as "dead" would
|
|
64
|
+
* report a running app as gone. A descriptor with no pid is not evidence of death either —
|
|
65
|
+
* it is absence of evidence, and the HTTP probe decides.
|
|
66
|
+
*/
|
|
67
|
+
export function processIsAlive(pid) {
|
|
68
|
+
if (pid === undefined)
|
|
69
|
+
return true;
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid, 0);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return error.code === 'EPERM';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=descriptors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descriptors.js","sourceRoot":"","sources":["../src/descriptors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAA;AAWnF,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,MAAc,qBAAqB;IACjE,IAAI,OAAiB,CAAA;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,mFAAmF;QACnF,oDAAoD;QACpD,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,WAAW,GAA2B,EAAE,CAAA;IAC9C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,SAAQ;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAkC,CAAA;YACtF,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;gBAAE,SAAQ;YAChF,WAAW,CAAC,IAAI,CAAC;gBACf,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;gBACpC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,GAAG,EAAE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;gBAC5D,IAAI;aACL,CAAC,CAAA;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,oFAAoF;YACpF,sBAAsB;YACtB,SAAQ;QACV,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAC/D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,GAAuB;IACpD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAClC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAA;IAC1D,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An MCP server over stdio that serves every running XENO application's capabilities.
|
|
3
|
+
*
|
|
4
|
+
* ## Why stdio and why no MCP SDK
|
|
5
|
+
*
|
|
6
|
+
* `PluginMcpManagerService` supports `stdio` and `sse`, and stdio is the right one here: the
|
|
7
|
+
* adapter must hold each app's bearer token, and a stdio child of the privileged host holds
|
|
8
|
+
* it in a process the renderer cannot reach. An `sse` server would be a second local HTTP
|
|
9
|
+
* port with its own auth story, to reach local HTTP ports that already have one.
|
|
10
|
+
*
|
|
11
|
+
* The wire protocol is JSON-RPC 2.0, newline-delimited, and the three methods a tool server
|
|
12
|
+
* needs are `initialize`, `tools/list` and `tools/call`. That is small enough that adding a
|
|
13
|
+
* dependency to get it would cost more than it saves — and this package is spawned by a
|
|
14
|
+
* signed host, so its dependency graph is part of what has to be trusted.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 **`tools/list` reports `inputSchema` as a COMPLETE JSON Schema**, per the MCP spec. The
|
|
17
|
+
* consuming SDK used to read it as a bare properties map and corrupt every tool's parameters
|
|
18
|
+
* (fixed in `xeno-agent-sdk` `src/mcp/tool-adapter.ts`). Do not "fix" this end to match an
|
|
19
|
+
* old client: a spec-shaped payload is the thing that is correct for every other client too.
|
|
20
|
+
*/
|
|
21
|
+
import { Readable, Writable } from 'node:stream';
|
|
22
|
+
import { type Catalogue, type DiscoverOptions } from './catalogue.js';
|
|
23
|
+
export declare const PROTOCOL_VERSION = "2024-11-05";
|
|
24
|
+
export declare const SERVER_NAME = "xeno-capabilities";
|
|
25
|
+
interface JsonRpcRequest {
|
|
26
|
+
jsonrpc: '2.0';
|
|
27
|
+
id?: string | number | null;
|
|
28
|
+
method: string;
|
|
29
|
+
params?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
type JsonRpcResponse = {
|
|
32
|
+
jsonrpc: '2.0';
|
|
33
|
+
id: string | number | null;
|
|
34
|
+
result: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
jsonrpc: '2.0';
|
|
37
|
+
id: string | number | null;
|
|
38
|
+
error: {
|
|
39
|
+
code: number;
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export interface McpServerOptions extends DiscoverOptions {
|
|
44
|
+
/** Injected so the whole request/response cycle can be exercised without a real app. */
|
|
45
|
+
discoverImpl?: ((options: DiscoverOptions) => Promise<Catalogue>) | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Handle one JSON-RPC request.
|
|
49
|
+
*
|
|
50
|
+
* Exported because this is where the protocol behaviour lives; a test that drives it through
|
|
51
|
+
* two pipes proves the plumbing, and a test that calls this proves the answers.
|
|
52
|
+
*/
|
|
53
|
+
export declare function handleRequest(request: JsonRpcRequest, options?: McpServerOptions): Promise<JsonRpcResponse | undefined>;
|
|
54
|
+
/** Run the server against a pair of streams until the input ends. */
|
|
55
|
+
export declare function serve(input?: Readable, output?: Writable, options?: McpServerOptions): Promise<void>;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=mcpServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpServer.d.ts","sourceRoot":"","sources":["../src/mcpServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAIhD,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAE9F,eAAO,MAAM,gBAAgB,eAAe,CAAA;AAC5C,eAAO,MAAM,WAAW,sBAAsB,CAAA;AAE9C,UAAU,cAAc;IACtB,OAAO,EAAE,KAAK,CAAA;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,KAAK,eAAe,GAChB;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/D;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAE5F,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,wFAAwF;IACxF,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC,GAAG,SAAS,CAAA;CAC9E;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAgHtC;AAED,qEAAqE;AACrE,wBAAgB,KAAK,CACnB,KAAK,GAAE,QAAwB,EAC/B,MAAM,GAAE,QAAyB,EACjC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An MCP server over stdio that serves every running XENO application's capabilities.
|
|
3
|
+
*
|
|
4
|
+
* ## Why stdio and why no MCP SDK
|
|
5
|
+
*
|
|
6
|
+
* `PluginMcpManagerService` supports `stdio` and `sse`, and stdio is the right one here: the
|
|
7
|
+
* adapter must hold each app's bearer token, and a stdio child of the privileged host holds
|
|
8
|
+
* it in a process the renderer cannot reach. An `sse` server would be a second local HTTP
|
|
9
|
+
* port with its own auth story, to reach local HTTP ports that already have one.
|
|
10
|
+
*
|
|
11
|
+
* The wire protocol is JSON-RPC 2.0, newline-delimited, and the three methods a tool server
|
|
12
|
+
* needs are `initialize`, `tools/list` and `tools/call`. That is small enough that adding a
|
|
13
|
+
* dependency to get it would cost more than it saves — and this package is spawned by a
|
|
14
|
+
* signed host, so its dependency graph is part of what has to be trusted.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 **`tools/list` reports `inputSchema` as a COMPLETE JSON Schema**, per the MCP spec. The
|
|
17
|
+
* consuming SDK used to read it as a bare properties map and corrupt every tool's parameters
|
|
18
|
+
* (fixed in `xeno-agent-sdk` `src/mcp/tool-adapter.ts`). Do not "fix" this end to match an
|
|
19
|
+
* old client: a spec-shaped payload is the thing that is correct for every other client too.
|
|
20
|
+
*/
|
|
21
|
+
import { createInterface } from 'node:readline';
|
|
22
|
+
import { Readable, Writable } from 'node:stream';
|
|
23
|
+
import { CapabilityClient, CapabilityUnavailable } from './capabilityClient.js';
|
|
24
|
+
import { readDescriptors } from './descriptors.js';
|
|
25
|
+
import { describeEntry, discover } from './catalogue.js';
|
|
26
|
+
export const PROTOCOL_VERSION = '2024-11-05';
|
|
27
|
+
export const SERVER_NAME = 'xeno-capabilities';
|
|
28
|
+
/**
|
|
29
|
+
* Handle one JSON-RPC request.
|
|
30
|
+
*
|
|
31
|
+
* Exported because this is where the protocol behaviour lives; a test that drives it through
|
|
32
|
+
* two pipes proves the plumbing, and a test that calls this proves the answers.
|
|
33
|
+
*/
|
|
34
|
+
export async function handleRequest(request, options = {}) {
|
|
35
|
+
const id = request.id ?? null;
|
|
36
|
+
// A notification (no id) gets no reply, ever. Replying to `notifications/initialized` with
|
|
37
|
+
// an id of null is a protocol violation some clients treat as fatal.
|
|
38
|
+
const isNotification = request.id === undefined;
|
|
39
|
+
switch (request.method) {
|
|
40
|
+
case 'initialize':
|
|
41
|
+
return {
|
|
42
|
+
jsonrpc: '2.0',
|
|
43
|
+
id,
|
|
44
|
+
result: {
|
|
45
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
46
|
+
capabilities: { tools: { listChanged: false } },
|
|
47
|
+
serverInfo: { name: SERVER_NAME, version: '0.1.30' },
|
|
48
|
+
instructions: 'Tools here drive XENO applications that are running on this machine right now. '
|
|
49
|
+
+ 'A tool named <app>_<domain>_<verb> acts on that application. If an application is '
|
|
50
|
+
+ 'not running its tools are absent, not failing — ask the user to open it.',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
case 'ping':
|
|
54
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
55
|
+
case 'tools/list': {
|
|
56
|
+
const catalogue = await (options.discoverImpl ?? discover)(options);
|
|
57
|
+
return {
|
|
58
|
+
jsonrpc: '2.0',
|
|
59
|
+
id,
|
|
60
|
+
result: {
|
|
61
|
+
tools: catalogue.entries.map((entry) => ({
|
|
62
|
+
name: entry.toolName,
|
|
63
|
+
description: describeEntry(entry),
|
|
64
|
+
inputSchema: entry.capability.schema,
|
|
65
|
+
})),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
case 'tools/call': {
|
|
70
|
+
const name = typeof request.params?.['name'] === 'string' ? request.params['name'] : '';
|
|
71
|
+
const args = request.params?.['arguments'] && typeof request.params['arguments'] === 'object'
|
|
72
|
+
? request.params['arguments']
|
|
73
|
+
: {};
|
|
74
|
+
const catalogue = await (options.discoverImpl ?? discover)(options);
|
|
75
|
+
const entry = catalogue.entries.find((candidate) => candidate.toolName === name);
|
|
76
|
+
if (!entry) {
|
|
77
|
+
/*
|
|
78
|
+
* A refusal has to say WHICH failure this is. "No such tool" and "the app that owns
|
|
79
|
+
* that tool is closed" lead to opposite next actions, and an agent told only the
|
|
80
|
+
* first will conclude the capability does not exist and stop trying.
|
|
81
|
+
*/
|
|
82
|
+
const app = name.split('_')[0] ?? '';
|
|
83
|
+
const down = catalogue.unreachable.find((candidate) => candidate.app === app);
|
|
84
|
+
const message = down
|
|
85
|
+
? `${app} is not available: ${down.reason}. Ask the user to open ${app}, then retry.`
|
|
86
|
+
: `No tool named ${name}. Running applications: ${[...new Set(catalogue.entries.map((candidate) => candidate.app))].join(', ') || 'none'}.`;
|
|
87
|
+
return { jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: message }] } };
|
|
88
|
+
}
|
|
89
|
+
const descriptor = readDescriptors(options.dir).find((candidate) => candidate.app === entry.app);
|
|
90
|
+
if (!descriptor) {
|
|
91
|
+
return {
|
|
92
|
+
jsonrpc: '2.0',
|
|
93
|
+
id,
|
|
94
|
+
result: {
|
|
95
|
+
isError: true,
|
|
96
|
+
content: [{ type: 'text', text: `${entry.app} stopped publishing itself between listing and this call.` }],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const client = new CapabilityClient(descriptor, {
|
|
101
|
+
fetchImpl: options.fetchImpl,
|
|
102
|
+
timeoutMs: options.timeoutMs,
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
const result = await client.invoke(entry.capability.name, args);
|
|
106
|
+
/*
|
|
107
|
+
* `isError` is derived from the application's own §8 envelope rather than assumed
|
|
108
|
+
* false. A refusal returned as a success is the fabricated-success shape this
|
|
109
|
+
* ecosystem keeps shipping, and here it would be invisible: the model would read a
|
|
110
|
+
* failure body as a result and carry on.
|
|
111
|
+
*/
|
|
112
|
+
const envelope = result;
|
|
113
|
+
const failed = envelope?.ok === false || envelope?.success === false || envelope?.isError === true;
|
|
114
|
+
return {
|
|
115
|
+
jsonrpc: '2.0',
|
|
116
|
+
id,
|
|
117
|
+
result: {
|
|
118
|
+
...(failed ? { isError: true } : {}),
|
|
119
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = error instanceof CapabilityUnavailable
|
|
125
|
+
? error.message
|
|
126
|
+
: `${entry.app} failed: ${error.message}`;
|
|
127
|
+
return { jsonrpc: '2.0', id, result: { isError: true, content: [{ type: 'text', text: message }] } };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
if (isNotification)
|
|
132
|
+
return undefined;
|
|
133
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${request.method}` } };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Run the server against a pair of streams until the input ends. */
|
|
137
|
+
export function serve(input = process.stdin, output = process.stdout, options = {}) {
|
|
138
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
139
|
+
/*
|
|
140
|
+
* Requests are answered in order. MCP allows concurrency, but these tools drive a user's
|
|
141
|
+
* open document: two overlapping edits arriving in an order nobody chose is worse than one
|
|
142
|
+
* being slightly slower, and the applications themselves serialise anyway.
|
|
143
|
+
*/
|
|
144
|
+
let queue = Promise.resolve();
|
|
145
|
+
lines.on('line', (line) => {
|
|
146
|
+
const text = line.trim();
|
|
147
|
+
if (!text)
|
|
148
|
+
return;
|
|
149
|
+
queue = queue.then(async () => {
|
|
150
|
+
let request;
|
|
151
|
+
try {
|
|
152
|
+
request = JSON.parse(text);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
output.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } })}\n`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const response = await handleRequest(request, options);
|
|
160
|
+
if (response)
|
|
161
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// A throw escaping here would leave the client waiting on an id forever, which reads
|
|
165
|
+
// as a hang rather than a failure it can act on.
|
|
166
|
+
if (request.id !== undefined) {
|
|
167
|
+
output.write(`${JSON.stringify({
|
|
168
|
+
jsonrpc: '2.0',
|
|
169
|
+
id: request.id ?? null,
|
|
170
|
+
error: { code: -32603, message: error.message },
|
|
171
|
+
})}\n`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
lines.on('close', () => {
|
|
178
|
+
queue.then(() => resolve()).catch(() => resolve());
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=mcpServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpServer.js","sourceRoot":"","sources":["../src/mcpServer.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xenosystem/capability-mcp",
|
|
3
|
+
"version": "0.1.31",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"description": "Presents every running XENO application's capability server to an agent as MCP tools.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"xeno-capability-mcp": "./dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./bin": {
|
|
18
|
+
"types": "./dist/bin.d.ts",
|
|
19
|
+
"import": "./dist/bin.js",
|
|
20
|
+
"default": "./dist/bin.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"scripts"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/XENO-CORPORATION/xeno-agent-interface.git",
|
|
33
|
+
"directory": "packages/capability-mcp"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prepack": "tsc -b",
|
|
37
|
+
"smoke:live": "node scripts/smoke-live-app.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate that decides whether tool inheritance actually works.
|
|
3
|
+
*
|
|
4
|
+
* Unit tests over a fake capability server prove the translation and never the connection —
|
|
5
|
+
* "built, tested, unreachable" is the defect this ecosystem has recorded seven times. So this
|
|
6
|
+
* spawns the REAL adapter binary as a child process, speaks REAL newline-delimited JSON-RPC
|
|
7
|
+
* to its stdin, and asserts a REAL XENO application changed as a result.
|
|
8
|
+
*
|
|
9
|
+
* node packages/capability-mcp/scripts/smoke-live-app.mjs
|
|
10
|
+
*
|
|
11
|
+
* It requires a XENO application to be running with its capability server open, e.g.
|
|
12
|
+
* XENO_CANVAS_CAPABILITY_SERVER=1 electron . --capability-server
|
|
13
|
+
*
|
|
14
|
+
* 🔴 It REFUSES rather than passes when nothing is running. A smoke test that reports success
|
|
15
|
+
* because it found nothing to test is the shape that let four uninstallable releases ship.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import { createInterface } from 'node:readline'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const BIN = fileURLToPath(new URL('../dist/bin.js', import.meta.url))
|
|
23
|
+
|
|
24
|
+
const gates = []
|
|
25
|
+
function gate(name, ok, detail = '') {
|
|
26
|
+
gates.push({ name, ok, detail })
|
|
27
|
+
process.stdout.write(` ${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}\n`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const child = spawn(process.execPath, [BIN], { stdio: ['pipe', 'pipe', 'inherit'] })
|
|
31
|
+
const lines = createInterface({ input: child.stdout })
|
|
32
|
+
const pending = new Map()
|
|
33
|
+
lines.on('line', (line) => {
|
|
34
|
+
if (!line.trim()) return
|
|
35
|
+
const message = JSON.parse(line)
|
|
36
|
+
const resolve = pending.get(message.id)
|
|
37
|
+
if (resolve) {
|
|
38
|
+
pending.delete(message.id)
|
|
39
|
+
resolve(message)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
let nextId = 1
|
|
44
|
+
function call(method, params) {
|
|
45
|
+
const id = nextId++
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
pending.set(id, resolve)
|
|
48
|
+
// A hang is a failure, not a wait: without this the script blocks forever on a child
|
|
49
|
+
// that stopped answering, and a run that neither passes nor fails gets retried instead
|
|
50
|
+
// of investigated.
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (pending.delete(id)) reject(new Error(`${method} did not answer in 60s`))
|
|
53
|
+
}, 60_000)
|
|
54
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const text = (response) => response.result?.content?.[0]?.text ?? ''
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const init = await call('initialize', { protocolVersion: '2024-11-05', capabilities: {} })
|
|
62
|
+
gate('the adapter completes an MCP handshake', Boolean(init.result?.protocolVersion), init.result?.serverInfo?.name)
|
|
63
|
+
|
|
64
|
+
const listed = await call('tools/list', {})
|
|
65
|
+
const tools = listed.result?.tools ?? []
|
|
66
|
+
const apps = [...new Set(tools.map((tool) => tool.name.split('_')[0]))]
|
|
67
|
+
gate('a running application is discovered', tools.length > 0, `${tools.length} tools from ${apps.join(', ') || 'nothing'}`)
|
|
68
|
+
if (tools.length === 0) {
|
|
69
|
+
console.error('\nNo XENO application is running with a capability server open.')
|
|
70
|
+
console.error('This is a REFUSAL, not a pass: nothing was proven.')
|
|
71
|
+
process.exit(2)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const withSchema = tools.find((tool) => Object.keys(tool.inputSchema?.properties ?? {}).length > 0)
|
|
75
|
+
gate(
|
|
76
|
+
'a tool arrives with a COMPLETE JSON Schema, not a properties map',
|
|
77
|
+
withSchema?.inputSchema?.type === 'object' && !('properties' in (withSchema.inputSchema.properties ?? {})),
|
|
78
|
+
withSchema?.name,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const has = (name) => tools.some((tool) => tool.name === name)
|
|
82
|
+
gate('the application exposes what this proof needs', has('canvas_document_create') && has('canvas_node_create_rectangle') && has('canvas_node_find'))
|
|
83
|
+
|
|
84
|
+
// 🔴 The gate that matters: an agent-side tool call must CHANGE the running application.
|
|
85
|
+
await call('tools/call', { name: 'canvas_document_create', arguments: {} })
|
|
86
|
+
const label = `mcp-smoke-${Date.now()}`
|
|
87
|
+
const created = await call('tools/call', {
|
|
88
|
+
name: 'canvas_node_create_rectangle',
|
|
89
|
+
arguments: { x: 24, y: 24, width: 120, height: 80, name: label },
|
|
90
|
+
})
|
|
91
|
+
gate('a tool call is not an error', created.result?.isError !== true, text(created).slice(0, 160))
|
|
92
|
+
|
|
93
|
+
const found = await call('tools/call', { name: 'canvas_node_find', arguments: { name: label } })
|
|
94
|
+
const body = text(found)
|
|
95
|
+
gate('the application actually changed — the node is there when read back', body.includes(label), body.slice(0, 200))
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* The refusal half, against an operation the application GENUINELY rejects.
|
|
99
|
+
*
|
|
100
|
+
* The first version of this gate asked for a node by an id that does not exist — Canvas
|
|
101
|
+
* answers `{ok:true, data:[]}` to that, so the assertion could not discriminate and passed
|
|
102
|
+
* on a working and a broken adapter alike. `page.set_current` with an unknown page really
|
|
103
|
+
* does answer `ok:false`, which is what makes the check able to fail.
|
|
104
|
+
*/
|
|
105
|
+
const refused = await call('tools/call', { name: 'canvas_page_set_current', arguments: { pageId: 'not-a-page' } })
|
|
106
|
+
gate(
|
|
107
|
+
"the application's own refusal arrives as isError, not as a result",
|
|
108
|
+
refused.result?.isError === true && /ok":false/.test(text(refused)),
|
|
109
|
+
text(refused).slice(0, 140),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const missing = await call('tools/call', { name: 'motion_timeline_play', arguments: {} })
|
|
113
|
+
gate(
|
|
114
|
+
'a closed application is named as CLOSED, not as a missing tool',
|
|
115
|
+
/not available|No tool named/.test(text(missing)),
|
|
116
|
+
text(missing).slice(0, 140),
|
|
117
|
+
)
|
|
118
|
+
} finally {
|
|
119
|
+
child.stdin.end()
|
|
120
|
+
child.kill()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const failed = gates.filter((entry) => !entry.ok)
|
|
124
|
+
process.stdout.write(`\n${gates.length - failed.length}/${gates.length} gates passed\n`)
|
|
125
|
+
process.exit(failed.length ? 1 : 0)
|