@impetik/xeer-mcp 0.2.2 → 0.2.4
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 +111 -38
- package/dist/dev-session.js +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +10 -2
- package/dist/network-policy.d.ts +4 -0
- package/dist/network-policy.js +65 -0
- package/dist/server.d.ts +7 -1
- package/dist/server.js +390 -199
- package/dist/test-run.js +3 -3
- package/dist/xeer-cli.d.ts +2 -2
- package/dist/xeer-cli.js +6 -6
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@impetik/xeer-mcp`
|
|
2
2
|
|
|
3
|
-
Model Context Protocol server for [Xeer](https://
|
|
4
|
-
scaffold → check →
|
|
5
|
-
`xeer.
|
|
3
|
+
Model Context Protocol server for [Xeer](https://docs.xeer.run), the framework for building and shipping
|
|
4
|
+
full-stack apps. It exposes the scaffold → check → test → build → deploy loop as MCP tools with a
|
|
5
|
+
stable `xeer.mcp-result.v0` result protocol.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[
|
|
10
|
-
|
|
7
|
+
📖 **Documentation: [docs.xeer.run](https://docs.xeer.run)** · **Building with AI agents:
|
|
8
|
+
[docs.xeer.run/guides/agents](https://docs.xeer.run/guides/agents)** · **Diagnostics:
|
|
9
|
+
[docs.xeer.run/reference/diagnostics](https://docs.xeer.run/reference/diagnostics)**
|
|
10
|
+
|
|
11
|
+
The [repository](https://github.com/impetik/xeer) keeps the canonical portable skill under
|
|
12
|
+
`.agents/skills/xeer/` and generates provider projections from it. `xeer new` installs those files;
|
|
13
|
+
for an existing project run `xeer agent setup`.
|
|
14
|
+
|
|
15
|
+
Start with `xeer://project/context` when the client supports MCP resources. Separate normalized project
|
|
16
|
+
resources live at `xeer://project/manifest`, `xeer://project/operations`, and `xeer://project/tests`;
|
|
17
|
+
`xeer://docs/index` lists installed-version Markdown pages exposed by `xeer://docs/{path}`, and
|
|
18
|
+
`xeer://diagnostics/{code}` resolves a stable diagnostic definition. Clients without resource support
|
|
19
|
+
can call `xeer_agent_context` and `xeer_docs_search` instead.
|
|
11
20
|
|
|
12
21
|
## Run it
|
|
13
22
|
|
|
@@ -17,40 +26,99 @@ the same protocol written as agent instructions.
|
|
|
17
26
|
"mcpServers": {
|
|
18
27
|
"xeer": {
|
|
19
28
|
"command": "npx",
|
|
20
|
-
"args": ["--package=@impetik/xeer-mcp", "--", "xeer-mcp"],
|
|
29
|
+
"args": ["--package=@impetik/xeer-mcp@<XEER_VERSION>", "--", "xeer-mcp"],
|
|
21
30
|
"env": { "XEER_MCP_ROOT": "." }
|
|
22
31
|
}
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
```
|
|
26
35
|
|
|
36
|
+
Prefer `xeer agent setup --target mcp`: it replaces `<XEER_VERSION>` and writes this configuration with the exact MCP package
|
|
37
|
+
version compatible with the installed CLI, records a generated-file hash, and refuses to overwrite an
|
|
38
|
+
unowned `.mcp.json`. `XEER_MCP_ROOT: "."` keeps all project paths confined to the checkout.
|
|
39
|
+
|
|
40
|
+
This starts the default `author` profile: `xeer_deploy_preview` always targets preview, and no
|
|
41
|
+
production-changing tool is registered. An operator who deliberately wants to expose reviewed
|
|
42
|
+
promotion must add `"XEER_MCP_PROFILE": "operator"` to the server environment. A tool argument cannot
|
|
43
|
+
change profiles, and operator promotion requires the exact `receiptId` returned by preview deploy. MCP
|
|
44
|
+
has no artifact-id or current-slot bypass.
|
|
45
|
+
|
|
27
46
|
From this repository, after `pnpm -r build`:
|
|
28
47
|
|
|
29
48
|
```sh
|
|
30
49
|
node packages/mcp/dist/main.js # stdio; or `pnpm mcp` from the workspace root
|
|
31
50
|
```
|
|
32
51
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`
|
|
52
|
+
## Registered action policy
|
|
53
|
+
|
|
54
|
+
The generated registry is the complete MCP tool and exclusion surface:
|
|
55
|
+
|
|
56
|
+
<!-- xeer-action-reference:start -->
|
|
57
|
+
<!-- Generated from packages/spec/src/actions.ts by `pnpm generate:agent-reference`. Do not edit. -->
|
|
58
|
+
|
|
59
|
+
**MCP tools (15 total; 14 author, 15 operator).** Inputs ending in `?` are optional.
|
|
60
|
+
|
|
61
|
+
| Tool | Action | Profiles | Summary | Inputs | Effects | Safety | Path policy | Output | Human prerequisite |
|
|
62
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
63
|
+
| `xeer_check` | `check` | `author` / `operator` | Validate a project and report structured diagnostics. | `directory?`: `string` | `read-source`<br>`write-generated` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
|
|
64
|
+
| `xeer_build` | `build` | `author` / `operator` | Build and verify a content-addressed application artifact. | `directory?`: `string` | `read-source`<br>`write-generated`<br>`run-local` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
|
|
65
|
+
| `xeer_test` | `test` | `author` / `operator` | Run application tests against fresh isolated local state. | `directory?`: `string`<br>`timeoutMilliseconds?`: `integer` [1000..1800000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
|
|
66
|
+
| `xeer_new` | `new` | `author` / `operator` | Create a new project from a supported scaffold. | `directory`: `string`<br>`template?`: `notes` / `todo` / `blog` / `personal-site` | `write-source` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
|
|
67
|
+
| `xeer_agent_context` | `agent.context` | `author` / `operator` | Read normalized project facts, operations, diagnostics, tests, and safe next actions. | `directory?`: `string` | `read-source` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.agent-context.v0` | none |
|
|
68
|
+
| `xeer_docs_search` | `docs.search` | `author` / `operator` | Search the installed-version Xeer documentation index. | `query`: `string`<br>`limit?`: `integer` [1..20]; default `5` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.docs-search.v0` | none |
|
|
69
|
+
| `xeer_doctor` | `doctor` | `author` / `operator` | Diagnose the toolchain and generated project state. | `directory?`: `string` | `read-source`<br>`write-generated` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | none |
|
|
70
|
+
| `xeer_deploy_preview` | `deploy.preview` | `author` / `operator` | Build and deploy an artifact to preview only. | `directory?`: `string`<br>`controlUrl?`: `string` | `read-source`<br>`write-generated`<br>`run-local`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | A human must establish the builder credential with xeer auth login. |
|
|
71
|
+
| `xeer_promote` | `promote.operator` | `operator` | Promote an exact review receipt in operator mode. | `directory?`: `string`<br>`receiptId`: `string`<br>`controlUrl?`: `string` | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | A human must review the exact artifact and start xeer-mcp with XEER_MCP_PROFILE=operator. |
|
|
72
|
+
| `xeer_auth_status` | `auth.status` | `author` / `operator` | Report builder credential metadata. | `controlUrl?`: `string` | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | none |
|
|
73
|
+
| `xeer_inspect` | `inspect` | `author` / `operator` | Read a running application manifest. | `previewUrl`: `string`<br>`view?`: `manifest` / `state` / `logs` / `export`; default `manifest`<br>`after?`: `string` | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | none |
|
|
74
|
+
| `xeer_dev_start` | `dev.start` | `author` / `operator` | Start a local development session. | `directory?`: `string`<br>`host?`: `string`<br>`port?`: `integer` [0..65535]<br>`timeoutMilliseconds?`: `integer` [1000..600000] | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | none |
|
|
75
|
+
| `xeer_dev_status` | `dev.status` | `author` / `operator` | Read new events from a local development session. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0`<br>`waitMilliseconds?`: `integer` [0..600000]; default `0`<br>`limit?`: `integer` [1..1000]; default `200` | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
|
|
76
|
+
| `xeer_dev_stop` | `dev.stop` | `author` / `operator` | Stop a local development session and release its lease. | `sessionId?`: `string`<br>`cursor?`: `integer` [0..9007199254740991]; default `0` | `run-local` | writes; idempotent; reversible; non-destructive | `none` | `xeer.dev.v0` | none |
|
|
77
|
+
| `xeer_diagnostics` | `diagnostics` | `author` / `operator` | Read the generated diagnostic catalogue. | `code?`: `string` | none | read-only; idempotent; reversible; non-destructive | `none` | `unversioned` | none |
|
|
78
|
+
|
|
79
|
+
**CLI actions intentionally excluded from MCP (31).**
|
|
80
|
+
|
|
81
|
+
| CLI action | Action | Summary | Effects | Safety | Path policy | Output | Why no MCP tool |
|
|
82
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
83
|
+
| `xeer agent setup` | `agent.setup` | Install or verify project-confined agent adapters. | `read-source`<br>`write-source` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
84
|
+
| `xeer deploy` | `deploy` | Build and deploy an application artifact. | `read-source`<br>`write-generated`<br>`run-local`<br>`network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | MCP exposes a separate preview-only deploy action; direct production deploy stays CLI-only. |
|
|
85
|
+
| `xeer promote` | `promote` | Promote a preview artifact to production. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | MCP promotion is a separate action available only when the server starts in operator profile. |
|
|
86
|
+
| `xeer link` | `link` | Link a checkout to an application the builder owns. | `read-source`<br>`write-source`<br>`network-read`<br>`network-write` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
|
|
87
|
+
| `xeer deployments` | `deployments` | List deployment history for an application. | `network-read`<br>`read-state`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
88
|
+
| `xeer rollback` | `rollback` | Redeploy a previously deployed artifact. | `network-read`<br>`network-write`<br>`production-change` | writes; non-idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
89
|
+
| `xeer disable` | `disable` | Take an application offline without deleting its data. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
90
|
+
| `xeer enable` | `enable` | Restore a disabled application to service. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
91
|
+
| `xeer delete` | `delete` | Permanently delete an application and all of its data. | `network-read`<br>`network-write`<br>`write-state`<br>`production-change` | writes; non-idempotent; irreversible; destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
92
|
+
| `xeer domains add` | `domains.add` | Attach a customer-owned hostname to an application. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
93
|
+
| `xeer domains ls` | `domains.ls` | List an application's platform and custom domains. | `network-read` | read-only; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
94
|
+
| `xeer domains status` | `domains.status` | Refresh hostname and certificate validation. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
95
|
+
| `xeer domains remove` | `domains.remove` | Detach a custom hostname and delete its provider certificate. | `network-read`<br>`network-write`<br>`production-change` | writes; idempotent; reversible; destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
96
|
+
| `xeer preview` | `preview` | Run a verified artifact in a local preview server. | `read-source`<br>`write-generated`<br>`run-local`<br>`write-state` | writes; non-idempotent; reversible; non-destructive | `project-relative` | `xeer.dev.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
97
|
+
| `xeer state` | `state.read` | Read record counts from a running application. | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
98
|
+
| `xeer state reset` | `state.reset` | Permanently reset local environment state. | `read-source`<br>`write-state` | writes; non-idempotent; irreversible; destructive | `project-relative` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
|
|
99
|
+
| `xeer logs` | `logs` | Read the bounded log ring of a running application. | `network-read`<br>`read-state` | read-only; idempotent; reversible; non-destructive | `app-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
100
|
+
| `xeer export` | `export` | Export application state to a document. | `network-read`<br>`read-state`<br>`write-file` | writes; idempotent; reversible; non-destructive | `project-or-url` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
101
|
+
| `xeer import` | `import` | Replace local or preview state from an export document. | `read-source`<br>`network-write`<br>`write-state` | writes; non-idempotent; irreversible; destructive | `project-or-url` | `xeer.command.v0` | Destructive state replacement requires an explicit CLI invocation. |
|
|
102
|
+
| `xeer auth login` | `auth.login` | Establish a human builder credential. | `network-read`<br>`network-write`<br>`secret-write` | writes; non-idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
|
|
103
|
+
| `xeer auth logout` | `auth.logout` | Remove the local builder credential. | `network-write`<br>`secret-write` | writes; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Human identity and project ownership decisions are not delegated through MCP. |
|
|
104
|
+
| `xeer auth as` | `auth.as` | Select a deterministic local development persona. | `write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
105
|
+
| `xeer auth clear` | `auth.clear` | Reset the local development persona. | `write-state` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
106
|
+
| `xeer env set` | `env.set` | Set an environment value or secret. | `network-write`<br>`write-state`<br>`secret-write` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
|
|
107
|
+
| `xeer env ls` | `env.ls` | List environment variable names and metadata. | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
|
|
108
|
+
| `xeer env rm` | `env.rm` | Remove an environment value or secret. | `network-write`<br>`write-state`<br>`secret-write` | writes; idempotent; irreversible; destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
|
|
109
|
+
| `xeer env pull` | `env.pull` | Write development secrets to a local file. | `network-read`<br>`write-file`<br>`secret-read` | writes; idempotent; reversible; non-destructive | `project-relative` | `xeer.command.v0` | Secret values are intentionally unreachable through MCP; env pull returns plaintext. |
|
|
110
|
+
| `xeer token create` | `token.create` | Issue a service builder token. | `network-write`<br>`secret-write` | writes; non-idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Service credential issuance and revocation are intentionally CLI-only. |
|
|
111
|
+
| `xeer token ls` | `token.ls` | List service token metadata. | `network-read`<br>`secret-metadata` | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Service credential issuance and revocation are intentionally CLI-only. |
|
|
112
|
+
| `xeer token revoke` | `token.revoke` | Revoke a service builder token. | `network-write`<br>`secret-write` | writes; idempotent; irreversible; destructive | `none` | `xeer.command.v0` | Service credential issuance and revocation are intentionally CLI-only. |
|
|
113
|
+
| `xeer actions` | `actions` | Print the machine-readable action and safety catalogue. | none | read-only; idempotent; reversible; non-destructive | `none` | `xeer.command.v0` | Not exposed through MCP v0; use the CLI deliberately. |
|
|
114
|
+
<!-- xeer-action-reference:end -->
|
|
115
|
+
|
|
116
|
+
Every tool returns `{ protocol: "xeer.mcp-result.v0", action, ok, result?, error? }` both as
|
|
117
|
+
`structuredContent` and as a JSON text block. Command tools preserve the CLI's
|
|
118
|
+
`xeer.command.v0` envelope at `result.envelope`, alongside `result.exitCode`. Host argv,
|
|
119
|
+
raw stderr, and absolute directories are omitted. A CLI crash still produces an `XE0000`
|
|
120
|
+
command envelope; an application-level refusal produces a stable `error.code` without
|
|
121
|
+
turning the response into an MCP transport error.
|
|
54
122
|
|
|
55
123
|
`xeer_test` is the exception, because `xeer test --json` streams `xeer.dev.v0` events rather
|
|
56
124
|
than printing an envelope. It collects the run and returns the summary plus the unabridged
|
|
@@ -59,11 +127,6 @@ assertion, `XE1905` throw or unexpectedly refused call, `XE1906` timeout) that i
|
|
|
59
127
|
matcher, expected and actual values, and the project-relative test location. It needs no
|
|
60
128
|
session handle: unlike `dev`, a test run is short-lived and terminal.
|
|
61
129
|
|
|
62
|
-
`xeer link` and `xeer env` are deliberately absent. Which hosted app a checkout deploys to
|
|
63
|
-
is the human owner's decision, so on `XE5111`/`XE5120`/`XE5121` the surface reports the
|
|
64
|
-
diagnostic and its documented repair instead of performing it. `xeer env` reads and writes
|
|
65
|
-
secrets — `xeer env pull` returns plaintext — so it is not reachable through a tool call.
|
|
66
|
-
|
|
67
130
|
## Why dev is three tools
|
|
68
131
|
|
|
69
132
|
An MCP tool call is request/response; `xeer dev` is a long-lived JSONL stream. The session
|
|
@@ -71,10 +134,10 @@ is therefore owned by the server and addressed by a `sessionId`, with the protoc
|
|
|
71
134
|
`seq` as a resumable cursor:
|
|
72
135
|
|
|
73
136
|
```text
|
|
74
|
-
xeer_dev_start → { sessionId, cursor, status: 'ready', preview: { url, inspectorUrl, ... }, events }
|
|
137
|
+
xeer_dev_start → { result: { sessionId, cursor, status: 'ready', preview: { url, inspectorUrl, ... }, events } }
|
|
75
138
|
edit a file
|
|
76
|
-
xeer_dev_status → { events: [compile.start, compile.diagnostic?, compile.ready?], openDiagnostics, generation }
|
|
77
|
-
xeer_dev_stop → { status: 'stopped' }
|
|
139
|
+
xeer_dev_status → { result: { events: [compile.start, compile.diagnostic?, compile.ready?], openDiagnostics, generation } }
|
|
140
|
+
xeer_dev_stop → { result: { status: 'stopped' } }
|
|
78
141
|
```
|
|
79
142
|
|
|
80
143
|
No event is summarized away, so the agent reads the same stream a terminal would show.
|
|
@@ -85,8 +148,18 @@ session exists for when the preview, the inspector, or rebuild/rollback behaviou
|
|
|
85
148
|
|
|
86
149
|
| Variable | Effect |
|
|
87
150
|
| --- | --- |
|
|
151
|
+
| `XEER_MCP_PROFILE` | `author` (default) exposes no production-changing action. `operator` additionally exposes exact-artifact `xeer_promote`; any other value refuses server startup. |
|
|
88
152
|
| `XEER_MCP_ROOT` | Confinement root for every `directory` argument. Defaults to the server's working directory; paths that escape it are refused. |
|
|
89
153
|
| `XEER_CLI` | Absolute path to `dist/cli.js`, overriding resolution through this package's dependencies. |
|
|
154
|
+
| `XEER_MCP_CONTROL_ORIGINS` | Comma-separated additional exact HTTPS or loopback development control-plane origins that MCP may send builder credentials to. Only hosted `https://control.xeer.run` is allowed by default; plaintext non-loopback origins are always refused. |
|
|
155
|
+
| `XEER_MCP_INSPECTOR_ORIGINS` | Comma-separated exact loopback HTTP origins for preview servers started outside this MCP server. A preview started with `xeer_dev_start` is allowed automatically only while that session remains active. |
|
|
156
|
+
|
|
157
|
+
URL-shaped inspector targets are confined separately: MCP accepts the loopback HTTP origin of an active
|
|
158
|
+
`xeer_dev_start` session (or an exact operator-configured origin) and exact hosted
|
|
159
|
+
`https://<app>.xeer.run` origins. Bare application names and appIds are safe because the authenticated
|
|
160
|
+
control plane resolves them; arbitrary, private-network,
|
|
161
|
+
link-local, custom-domain, and credential-bearing URLs are refused before the CLI runs. Use an app
|
|
162
|
+
name or appId to inspect an application that serves a custom domain.
|
|
90
163
|
|
|
91
164
|
Always stop dev sessions you start: local state is leased per project and mode, so a leaked
|
|
92
165
|
session makes the next run fail with `XE1812`. The stdio entrypoint stops every session on
|
package/dist/dev-session.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
2
|
import { resolveXeerCli } from './xeer-cli.js';
|
|
3
|
+
function hasPreviewUrls(value) {
|
|
4
|
+
if (typeof value !== 'object' || value === null)
|
|
5
|
+
return false;
|
|
6
|
+
const candidate = value;
|
|
7
|
+
return ['url', 'healthUrl', 'inspectorUrl', 'debugUrl', 'logsUrl']
|
|
8
|
+
.every((key) => typeof candidate[key] === 'string');
|
|
9
|
+
}
|
|
3
10
|
const MAX_BUFFERED_EVENTS = 2_000;
|
|
4
11
|
const MAX_STDERR_CHARACTERS = 4_096;
|
|
5
12
|
const DEFAULT_START_TIMEOUT_MILLISECONDS = 180_000;
|
|
@@ -123,6 +130,11 @@ export class DevSession {
|
|
|
123
130
|
this.#compiling = true;
|
|
124
131
|
break;
|
|
125
132
|
case 'server.restarted':
|
|
133
|
+
// A Vite restart may bind a different port when automation requested port 0. The URLs on
|
|
134
|
+
// a current event replace preview.ready immediately. Keep the last valid discovery values
|
|
135
|
+
// when supervising an older CLI whose pre-v0 payload did not yet carry URLs.
|
|
136
|
+
if (hasPreviewUrls(event.data))
|
|
137
|
+
this.#preview = event.data;
|
|
126
138
|
this.#restarting = false;
|
|
127
139
|
this.#compiling = false;
|
|
128
140
|
break;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createXeerMcpServer, type XeerMcpServer } from './server.js';
|
|
1
|
+
export { createXeerMcpServer, mcpProfile, type XeerMcpServer, type XeerMcpServerOptions, } from './server.js';
|
|
2
2
|
export { DevSession, DevSessionRegistry, type DevSessionOptions, type DevSessionRead, type DevSessionStatus, type DevSessionSummary, type PreviewUrls, } from './dev-session.js';
|
|
3
3
|
export { runXeerTests, type RunTestsOptions, type TestCaseResult, type TestFailure, type TestRunResult, } from './test-run.js';
|
|
4
4
|
export { projectRoot, resolveDirectory, resolveXeerCli, runXeerCommand, XeerCliError, type CommandEnvelope, type CommandRun, type RunOptions, } from './xeer-cli.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createXeerMcpServer } from './server.js';
|
|
1
|
+
export { createXeerMcpServer, mcpProfile, } from './server.js';
|
|
2
2
|
export { DevSession, DevSessionRegistry, } from './dev-session.js';
|
|
3
3
|
export { runXeerTests, } from './test-run.js';
|
|
4
4
|
export { projectRoot, resolveDirectory, resolveXeerCli, runXeerCommand, XeerCliError, } from './xeer-cli.js';
|
package/dist/main.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { createXeerMcpServer } from './server.js';
|
|
3
|
+
import { createXeerMcpServer, mcpProfile } from './server.js';
|
|
4
4
|
/**
|
|
5
5
|
* stdio entrypoint. Nothing may be written to stdout except JSON-RPC frames, so
|
|
6
6
|
* every diagnostic message from this process goes to stderr.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
let instance;
|
|
9
|
+
try {
|
|
10
|
+
instance = createXeerMcpServer({ profile: mcpProfile(process.env.XEER_MCP_PROFILE) });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
process.stderr.write(`xeer-mcp failed to start: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const { server, devSessions } = instance;
|
|
9
17
|
let closing = false;
|
|
10
18
|
async function shutdown(code) {
|
|
11
19
|
if (closing)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Undefined means the explicit control origin is safe to receive the builder credential. */
|
|
2
|
+
export declare function controlOriginRefusal(value: string | undefined, configured?: string | undefined): string | undefined;
|
|
3
|
+
/** Undefined means the target cannot make MCP perform an arbitrary direct network read. */
|
|
4
|
+
export declare function inspectorTargetRefusal(value: string, activeLocalOrigins?: readonly string[], configured?: string | undefined): string | undefined;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DEFAULT_XEER_CONTROL_ORIGIN, exactHttpOrigin, isHostedXeerApplicationTarget, isLocalInspectorTarget, isLoopbackHostname, } from '@impetik/xeer-spec/network-policy';
|
|
2
|
+
const APP_ID = /^app_[A-Za-z0-9_-]{8,96}$/u;
|
|
3
|
+
const APPLICATION_NAME = /^[a-z][a-z0-9-]{1,62}$/u;
|
|
4
|
+
function configuredControlOrigins(value) {
|
|
5
|
+
const result = new Set();
|
|
6
|
+
for (const entry of value?.split(',') ?? []) {
|
|
7
|
+
const origin = exactHttpOrigin(entry);
|
|
8
|
+
if (!origin)
|
|
9
|
+
continue;
|
|
10
|
+
const url = new URL(origin);
|
|
11
|
+
// Configuration can add an HTTPS control plane or a loopback development one; it can never opt
|
|
12
|
+
// a plaintext non-loopback endpoint into receiving a credential.
|
|
13
|
+
if (url.protocol === 'https:' || isLoopbackHostname(url.hostname))
|
|
14
|
+
result.add(origin);
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function configuredInspectorOrigins(value) {
|
|
19
|
+
const result = new Set();
|
|
20
|
+
for (const entry of value?.split(',') ?? []) {
|
|
21
|
+
const origin = exactHttpOrigin(entry);
|
|
22
|
+
if (!origin)
|
|
23
|
+
continue;
|
|
24
|
+
const url = new URL(origin);
|
|
25
|
+
if (url.protocol === 'http:' && isLoopbackHostname(url.hostname))
|
|
26
|
+
result.add(origin);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/** Undefined means the explicit control origin is safe to receive the builder credential. */
|
|
31
|
+
export function controlOriginRefusal(value, configured = process.env.XEER_MCP_CONTROL_ORIGINS) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const origin = exactHttpOrigin(value);
|
|
35
|
+
if (!origin) {
|
|
36
|
+
return 'The MCP controlUrl must be an exact HTTP(S) origin with no credentials, path, query, or fragment.';
|
|
37
|
+
}
|
|
38
|
+
if (origin === DEFAULT_XEER_CONTROL_ORIGIN || configuredControlOrigins(configured).has(origin))
|
|
39
|
+
return undefined;
|
|
40
|
+
return 'The MCP controlUrl is not allowed to receive builder credentials. Use https://control.xeer.run, '
|
|
41
|
+
+ 'or preconfigure the exact HTTPS or loopback development origin in XEER_MCP_CONTROL_ORIGINS.';
|
|
42
|
+
}
|
|
43
|
+
/** Undefined means the target cannot make MCP perform an arbitrary direct network read. */
|
|
44
|
+
export function inspectorTargetRefusal(value, activeLocalOrigins = [], configured = process.env.XEER_MCP_INSPECTOR_ORIGINS) {
|
|
45
|
+
const target = value.trim();
|
|
46
|
+
if (!target)
|
|
47
|
+
return 'The inspector target must not be empty.';
|
|
48
|
+
if (isLocalInspectorTarget(target)) {
|
|
49
|
+
const origin = new URL(target).origin;
|
|
50
|
+
const allowed = new Set([
|
|
51
|
+
...activeLocalOrigins.map((entry) => exactHttpOrigin(entry)).filter((entry) => entry !== undefined),
|
|
52
|
+
...configuredInspectorOrigins(configured),
|
|
53
|
+
]);
|
|
54
|
+
if (allowed.has(origin))
|
|
55
|
+
return undefined;
|
|
56
|
+
return 'The loopback inspector origin is not an active MCP dev session. Start it with xeer_dev_start '
|
|
57
|
+
+ 'or preconfigure the exact origin in XEER_MCP_INSPECTOR_ORIGINS.';
|
|
58
|
+
}
|
|
59
|
+
// Hosted URLs are not fetched directly: the CLI encodes them into the authenticated control-plane
|
|
60
|
+
// lookup path, which owner-resolves the application before proxying its inspector.
|
|
61
|
+
if (isHostedXeerApplicationTarget(target) || APP_ID.test(target) || APPLICATION_NAME.test(target))
|
|
62
|
+
return undefined;
|
|
63
|
+
return 'MCP inspector URLs must be loopback HTTP URLs from xeer dev/preview or exact hosted '
|
|
64
|
+
+ 'https://<app>.xeer.run origins. Use the application name or appId for a custom hosted domain.';
|
|
65
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type XeerMcpProfile } from '@impetik/xeer-spec/actions';
|
|
2
3
|
import { DevSessionRegistry } from './dev-session.js';
|
|
3
4
|
export interface XeerMcpServer {
|
|
4
5
|
server: McpServer;
|
|
5
6
|
devSessions: DevSessionRegistry;
|
|
7
|
+
profile: XeerMcpProfile;
|
|
6
8
|
}
|
|
7
|
-
export
|
|
9
|
+
export interface XeerMcpServerOptions {
|
|
10
|
+
readonly profile?: XeerMcpProfile;
|
|
11
|
+
}
|
|
12
|
+
export declare function mcpProfile(value: string | undefined): XeerMcpProfile;
|
|
13
|
+
export declare function createXeerMcpServer(options?: XeerMcpServerOptions): XeerMcpServer;
|
package/dist/server.js
CHANGED
|
@@ -1,23 +1,86 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import {
|
|
2
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
3
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import { z } from 'zod';
|
|
5
|
+
import { createAgentContext, readDocsIndex, readDocsPage, searchDocs } from '@impetik/xeer/agent-context';
|
|
4
6
|
import { DIAGNOSTICS, DIAGNOSTIC_CODES, DIAGNOSTIC_FAMILIES, diagnosticDefinition, renderDiagnosticsReference, } from '@impetik/xeer-spec/diagnostics';
|
|
7
|
+
import { actionDefinition, mcpAnnotations, } from '@impetik/xeer-spec/actions';
|
|
8
|
+
import { isReviewReceiptId } from '@impetik/xeer-spec/review';
|
|
5
9
|
import { DevSessionRegistry } from './dev-session.js';
|
|
10
|
+
import { controlOriginRefusal, inspectorTargetRefusal } from './network-policy.js';
|
|
6
11
|
import { runXeerTests } from './test-run.js';
|
|
7
|
-
import { projectRoot, resolveDirectory, runXeerCommand, XeerCliError } from './xeer-cli.js';
|
|
12
|
+
import { projectRoot, resolveDirectory, runXeerCommand, XeerCliError, } from './xeer-cli.js';
|
|
8
13
|
/**
|
|
9
14
|
* The Xeer MCP server.
|
|
10
15
|
*
|
|
11
16
|
* Tools are a thin, honest projection of the CLI: one tool per command, each
|
|
12
|
-
*
|
|
17
|
+
* preserving the `xeer.command.v0` envelope inside `xeer.mcp-result.v0`. The tools that
|
|
13
18
|
* are not a single command are the dev-session trio, because a long-running
|
|
14
19
|
* event stream has to be addressed by a handle and a cursor, and
|
|
15
20
|
* `xeer_diagnostics`, which serves the generated catalogue so an agent can look
|
|
16
21
|
* up a code without a second round trip through the filesystem.
|
|
17
22
|
*/
|
|
18
23
|
const version = createRequire(import.meta.url)('../package.json').version;
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
function inputPropertySchema(property) {
|
|
25
|
+
let schema;
|
|
26
|
+
if (property.type === 'string') {
|
|
27
|
+
if (property.enum?.length === 0)
|
|
28
|
+
throw new Error('MCP input enum must contain at least one value.');
|
|
29
|
+
if (property.enum) {
|
|
30
|
+
if (property.minLength !== undefined || property.maxLength !== undefined || property.pattern !== undefined) {
|
|
31
|
+
throw new Error('MCP string enum inputs cannot also declare length or pattern constraints.');
|
|
32
|
+
}
|
|
33
|
+
schema = z.enum(property.enum);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
let string = z.string();
|
|
37
|
+
if (property.minLength !== undefined)
|
|
38
|
+
string = string.min(property.minLength);
|
|
39
|
+
if (property.maxLength !== undefined)
|
|
40
|
+
string = string.max(property.maxLength);
|
|
41
|
+
if (property.pattern !== undefined)
|
|
42
|
+
string = string.regex(new RegExp(property.pattern, 'u'));
|
|
43
|
+
schema = string;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (property.type === 'boolean') {
|
|
47
|
+
schema = z.boolean();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
let number = z.number();
|
|
51
|
+
if (property.type === 'integer')
|
|
52
|
+
number = number.int();
|
|
53
|
+
if (property.minimum !== undefined)
|
|
54
|
+
number = number.min(property.minimum);
|
|
55
|
+
if (property.maximum !== undefined)
|
|
56
|
+
number = number.max(property.maximum);
|
|
57
|
+
schema = number;
|
|
58
|
+
}
|
|
59
|
+
if (property.description !== undefined)
|
|
60
|
+
schema = schema.describe(property.description);
|
|
61
|
+
return schema;
|
|
62
|
+
}
|
|
63
|
+
/** Runtime MCP presentation and validation come from the registry projected by listTools and the docs. */
|
|
64
|
+
function toolPolicy(id) {
|
|
65
|
+
const definition = actionDefinition(id);
|
|
66
|
+
const registered = definition?.surfaces.mcpInputSchema;
|
|
67
|
+
if (!registered)
|
|
68
|
+
throw new Error(`Action ${id} has no registered MCP input schema.`);
|
|
69
|
+
const title = definition.surfaces.mcpTitle;
|
|
70
|
+
const description = definition.surfaces.mcpDescription;
|
|
71
|
+
if (!title || !description)
|
|
72
|
+
throw new Error(`Action ${id} has no registered MCP presentation.`);
|
|
73
|
+
const required = new Set(registered.required ?? []);
|
|
74
|
+
const inputSchema = Object.fromEntries(Object.entries(registered.properties).map(([name, property]) => {
|
|
75
|
+
let schema = inputPropertySchema(property);
|
|
76
|
+
if (property.default !== undefined)
|
|
77
|
+
schema = schema.default(property.default);
|
|
78
|
+
else if (!required.has(name))
|
|
79
|
+
schema = schema.optional();
|
|
80
|
+
return [name, schema];
|
|
81
|
+
}));
|
|
82
|
+
return { title, description, inputSchema, annotations: mcpAnnotations(definition) };
|
|
83
|
+
}
|
|
21
84
|
/** Mirrors the Diagnostic interface in @impetik/xeer-spec, loosely so new fields pass through. */
|
|
22
85
|
const diagnosticSchema = z.object({
|
|
23
86
|
code: z.string(),
|
|
@@ -31,107 +94,232 @@ const diagnosticSchema = z.object({
|
|
|
31
94
|
}).loose().optional(),
|
|
32
95
|
hint: z.string().optional(),
|
|
33
96
|
}).loose();
|
|
34
|
-
const
|
|
35
|
-
argv: z.array(z.string()).describe('The exact xeer argv that produced this envelope.'),
|
|
36
|
-
exitCode: z.number().nullable(),
|
|
97
|
+
const commandEnvelopeOutput = {
|
|
37
98
|
protocol: z.literal('xeer.command.v0'),
|
|
38
99
|
command: z.string(),
|
|
39
100
|
ok: z.boolean(),
|
|
40
101
|
diagnostics: z.array(diagnosticSchema),
|
|
41
102
|
result: z.unknown().optional(),
|
|
42
|
-
stderr: z.string().optional(),
|
|
43
103
|
};
|
|
44
|
-
|
|
104
|
+
const mcpErrorSchema = z.object({ code: z.string(), message: z.string() });
|
|
105
|
+
const wrappedOutput = (result) => ({
|
|
106
|
+
protocol: z.literal('xeer.mcp-result.v0'),
|
|
107
|
+
action: z.string(),
|
|
108
|
+
ok: z.boolean(),
|
|
109
|
+
result: result.optional(),
|
|
110
|
+
error: mcpErrorSchema.optional(),
|
|
111
|
+
});
|
|
112
|
+
const commandOutput = wrappedOutput(z.object({
|
|
113
|
+
exitCode: z.number().nullable(),
|
|
114
|
+
envelope: z.object(commandEnvelopeOutput).loose(),
|
|
115
|
+
}));
|
|
116
|
+
function structured(action, result, options = {}) {
|
|
117
|
+
const reusableResult = reusableValue(result);
|
|
118
|
+
const payload = {
|
|
119
|
+
protocol: 'xeer.mcp-result.v0',
|
|
120
|
+
action,
|
|
121
|
+
ok: options.ok ?? true,
|
|
122
|
+
...(reusableResult === undefined ? {} : { result: reusableResult }),
|
|
123
|
+
...(options.error ? { error: reusableValue(options.error) } : {}),
|
|
124
|
+
};
|
|
45
125
|
return {
|
|
46
126
|
// The text block is the same JSON, because a client that ignores
|
|
47
|
-
// structuredContent must still see the whole
|
|
127
|
+
// structuredContent must still see the whole wrapper.
|
|
48
128
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
49
129
|
structuredContent: payload,
|
|
50
|
-
...(payload
|
|
130
|
+
...(payload.ok === false ? { isError: false } : {}),
|
|
51
131
|
};
|
|
52
132
|
}
|
|
53
|
-
function failure(message) {
|
|
54
|
-
return
|
|
133
|
+
function failure(action, code, message) {
|
|
134
|
+
return structured(action, undefined, { ok: false, error: { code, message } });
|
|
135
|
+
}
|
|
136
|
+
function escapeRegularExpression(value) {
|
|
137
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
138
|
+
}
|
|
139
|
+
/** Removes the configured host root from every public string, including nested diagnostics/events. */
|
|
140
|
+
function reusableString(value) {
|
|
141
|
+
const root = projectRoot();
|
|
142
|
+
const roots = new Set([root, root.split('\\').join('/'), root.split('/').join('\\')]);
|
|
143
|
+
let output = value;
|
|
144
|
+
for (const candidate of roots) {
|
|
145
|
+
output = output.replace(new RegExp(escapeRegularExpression(candidate), 'gi'), '.');
|
|
146
|
+
}
|
|
147
|
+
return output;
|
|
148
|
+
}
|
|
149
|
+
function reusableValue(value) {
|
|
150
|
+
if (typeof value === 'string')
|
|
151
|
+
return reusableString(value);
|
|
152
|
+
if (Array.isArray(value))
|
|
153
|
+
return value.map(reusableValue);
|
|
154
|
+
if (typeof value !== 'object' || value === null)
|
|
155
|
+
return value;
|
|
156
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, reusableValue(entry)]));
|
|
157
|
+
}
|
|
158
|
+
function projectPath(path) {
|
|
159
|
+
const output = relative(projectRoot(), path).split(sep).join('/');
|
|
160
|
+
return output || '.';
|
|
161
|
+
}
|
|
162
|
+
function reusableEnvelope(envelope) {
|
|
163
|
+
if (envelope.command !== 'new' || typeof envelope.result !== 'object' || envelope.result === null)
|
|
164
|
+
return envelope;
|
|
165
|
+
const result = envelope.result;
|
|
166
|
+
return {
|
|
167
|
+
...envelope,
|
|
168
|
+
result: {
|
|
169
|
+
...result,
|
|
170
|
+
...(typeof result['directory'] === 'string'
|
|
171
|
+
? { directory: isAbsolute(result['directory']) ? projectPath(result['directory']) : result['directory'] }
|
|
172
|
+
: {}),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
55
175
|
}
|
|
56
|
-
/** Runs a CLI command and
|
|
57
|
-
async function commandTool(args, directory) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
176
|
+
/** Runs a CLI command and wraps its envelope without exposing host argv or stderr. */
|
|
177
|
+
async function commandTool(action, args, directory) {
|
|
178
|
+
let run;
|
|
179
|
+
try {
|
|
180
|
+
run = await runXeerCommand(args, { cwd: directory });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof XeerCliError)
|
|
184
|
+
return failure(action, 'adapter_unavailable', error.message);
|
|
185
|
+
return failure(action, 'adapter_failure', 'The xeer CLI adapter failed before producing a result.');
|
|
186
|
+
}
|
|
187
|
+
const envelope = reusableEnvelope(run.envelope);
|
|
188
|
+
const adapterDiagnostic = envelope.diagnostics.find((diagnostic) => diagnostic.code === 'XE0000');
|
|
189
|
+
return structured(action, {
|
|
61
190
|
exitCode: run.exitCode,
|
|
62
|
-
|
|
63
|
-
|
|
191
|
+
envelope,
|
|
192
|
+
}, {
|
|
193
|
+
ok: envelope.ok,
|
|
194
|
+
...(adapterDiagnostic ? {
|
|
195
|
+
error: { code: 'adapter_failure', message: adapterDiagnostic.message },
|
|
196
|
+
} : {}),
|
|
64
197
|
});
|
|
65
198
|
}
|
|
66
|
-
async function withDirectory(directory, body) {
|
|
199
|
+
async function withDirectory(directory, action, body) {
|
|
200
|
+
let resolved;
|
|
67
201
|
try {
|
|
68
|
-
|
|
202
|
+
resolved = await resolveDirectory(directory);
|
|
69
203
|
}
|
|
70
204
|
catch (error) {
|
|
71
205
|
if (error instanceof XeerCliError)
|
|
72
|
-
return failure(error.message);
|
|
206
|
+
return failure(action, 'path_refused', error.message);
|
|
73
207
|
throw error;
|
|
74
208
|
}
|
|
209
|
+
return body(resolved);
|
|
210
|
+
}
|
|
211
|
+
export function mcpProfile(value) {
|
|
212
|
+
if (value === undefined || value.trim() === '')
|
|
213
|
+
return 'author';
|
|
214
|
+
if (value === 'author' || value === 'operator')
|
|
215
|
+
return value;
|
|
216
|
+
throw new Error(`Invalid XEER_MCP_PROFILE ${JSON.stringify(value)}; expected "author" or "operator".`);
|
|
75
217
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'read diagnostics, apply the smallest edit each one names, run it again, converge on ok: true.',
|
|
79
|
-
'Runs the manifest stage and, if that passes, the module-graph/zone/operations/type analysis.',
|
|
80
|
-
'Envelope: { protocol: "xeer.command.v0", command: "check", ok, diagnostics: Diagnostic[],',
|
|
81
|
-
'result?: { manifest } }. Diagnostic codes are stable — look one up with xeer_diagnostics.',
|
|
82
|
-
].join(' ');
|
|
83
|
-
const BUILD_DESCRIPTION = [
|
|
84
|
-
'Build the content-addressed artifact. Runs check first, so build diagnostics are a superset',
|
|
85
|
-
'of check diagnostics plus bundling, budget, and asset codes (XE14xx, XE15xx).',
|
|
86
|
-
'On success result is { artifactId, outputDirectory, modules, assets, operations }.',
|
|
87
|
-
'Run xeer_test before this to prove the application behaves, not just that it compiles.',
|
|
88
|
-
].join(' ');
|
|
89
|
-
export function createXeerMcpServer() {
|
|
218
|
+
export function createXeerMcpServer(options = {}) {
|
|
219
|
+
const profile = options.profile ?? 'author';
|
|
90
220
|
const server = new McpServer({ name: 'xeer', version }, {
|
|
91
221
|
instructions: [
|
|
92
222
|
'Xeer is a constrained full-stack platform whose diagnostics are its agent surface.',
|
|
93
|
-
|
|
223
|
+
'This server is confined to its configured project root.',
|
|
94
224
|
'Loop: xeer_new to scaffold, xeer_check after every edit, xeer_test to prove behaviour,',
|
|
95
|
-
'xeer_build
|
|
96
|
-
'
|
|
225
|
+
'xeer_build, then xeer_deploy_preview. A green check means it compiles; only a green test means it works.',
|
|
226
|
+
'The author profile cannot change production. Promotion is available only when a human starts',
|
|
227
|
+
'the server with XEER_MCP_PROFILE=operator, and then requires an exact artifact id.',
|
|
228
|
+
'Tools return xeer.mcp-result.v0; command results preserve the CLI xeer.command.v0 envelope',
|
|
229
|
+
'under result.envelope. Dispatch on that envelope\'s diagnostics[].code',
|
|
97
230
|
'and use xeer_diagnostics to learn what a code means and which edit closes it.',
|
|
98
231
|
'Use xeer_dev_start/status/stop only when you need a running preview and inspector;',
|
|
99
232
|
'a check-edit-check loop plus xeer_test is enough to build and repair a project.',
|
|
100
233
|
].join(' '),
|
|
101
234
|
});
|
|
102
235
|
const devSessions = new DevSessionRegistry();
|
|
236
|
+
const jsonResource = (uri, value) => ({
|
|
237
|
+
contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(reusableValue(value), null, 2) }],
|
|
238
|
+
});
|
|
239
|
+
const contextResource = async (uri, select) => jsonResource(uri, select(await createAgentContext(projectRoot())));
|
|
240
|
+
server.registerResource('project-context', 'xeer://project/context', {
|
|
241
|
+
title: 'Normalized Xeer project context', description: 'Bounded, deterministic, read-only AgentContextV0.',
|
|
242
|
+
mimeType: 'application/json',
|
|
243
|
+
}, async (uri) => contextResource(uri, (context) => context));
|
|
244
|
+
server.registerResource('project-manifest', 'xeer://project/manifest', {
|
|
245
|
+
title: 'Normalized application manifest', mimeType: 'application/json',
|
|
246
|
+
}, async (uri) => contextResource(uri, (context) => ({
|
|
247
|
+
...context.application,
|
|
248
|
+
capabilities: context.capabilities,
|
|
249
|
+
storage: context.storage,
|
|
250
|
+
budgets: context.budgets,
|
|
251
|
+
database: context.schema,
|
|
252
|
+
})));
|
|
253
|
+
server.registerResource('project-operations', 'xeer://project/operations', {
|
|
254
|
+
title: 'Discovered application operations', mimeType: 'application/json',
|
|
255
|
+
}, async (uri) => contextResource(uri, (context) => context.operations));
|
|
256
|
+
server.registerResource('project-tests', 'xeer://project/tests', {
|
|
257
|
+
title: 'Application test inventory', mimeType: 'application/json',
|
|
258
|
+
}, async (uri) => contextResource(uri, (context) => context.tests));
|
|
259
|
+
server.registerResource('docs-index', 'xeer://docs/index', {
|
|
260
|
+
title: 'Installed Xeer documentation index', description: 'Compact version-matched page and content-hash index.',
|
|
261
|
+
mimeType: 'application/json',
|
|
262
|
+
}, async (uri) => {
|
|
263
|
+
const docs = await readDocsIndex();
|
|
264
|
+
return jsonResource(uri, {
|
|
265
|
+
...docs.index,
|
|
266
|
+
...(docs.compatibilityWarning ? { compatibilityWarning: docs.compatibilityWarning } : {}),
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
const docsTemplate = new ResourceTemplate('xeer://docs/{+path}', {
|
|
270
|
+
list: async () => {
|
|
271
|
+
const { index } = await readDocsIndex();
|
|
272
|
+
return { resources: index.pages.map((page) => ({
|
|
273
|
+
uri: `xeer://docs${page.path === '/' ? '/' : page.path}`,
|
|
274
|
+
name: page.title,
|
|
275
|
+
title: page.title,
|
|
276
|
+
description: page.description,
|
|
277
|
+
mimeType: 'text/markdown',
|
|
278
|
+
})) };
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
server.registerResource('docs-page', docsTemplate, {
|
|
282
|
+
title: 'Installed Xeer documentation page', description: 'A version-matched Markdown page loaded on demand.',
|
|
283
|
+
mimeType: 'text/markdown',
|
|
284
|
+
}, async (uri, variables) => {
|
|
285
|
+
const raw = variables.path;
|
|
286
|
+
const path = Array.isArray(raw) ? raw.join('/') : String(raw ?? '');
|
|
287
|
+
const page = await readDocsPage(path);
|
|
288
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text: page.markdown }] };
|
|
289
|
+
});
|
|
290
|
+
const diagnosticsTemplate = new ResourceTemplate('xeer://diagnostics/{code}', { list: undefined });
|
|
291
|
+
server.registerResource('diagnostic', diagnosticsTemplate, {
|
|
292
|
+
title: 'Xeer diagnostic definition', mimeType: 'application/json',
|
|
293
|
+
}, async (uri, variables) => {
|
|
294
|
+
const code = String(variables.code ?? '').toUpperCase();
|
|
295
|
+
const definition = diagnosticDefinition(code);
|
|
296
|
+
if (!definition)
|
|
297
|
+
throw new Error(`Unknown Xeer diagnostic code: ${code}`);
|
|
298
|
+
return jsonResource(uri, definition);
|
|
299
|
+
});
|
|
300
|
+
server.registerTool('xeer_agent_context', {
|
|
301
|
+
...toolPolicy('agent.context'),
|
|
302
|
+
outputSchema: wrappedOutput(z.object({ protocol: z.literal('xeer.agent-context.v0') }).loose()),
|
|
303
|
+
}, async ({ directory }) => withDirectory(directory, 'agent.context', async (resolved) => structured('agent.context', await createAgentContext(resolved))));
|
|
304
|
+
server.registerTool('xeer_docs_search', {
|
|
305
|
+
...toolPolicy('docs.search'),
|
|
306
|
+
outputSchema: wrappedOutput(z.object({
|
|
307
|
+
protocol: z.literal('xeer.docs-search.v0'), query: z.string(), results: z.array(z.object({
|
|
308
|
+
path: z.string(), uri: z.string(), title: z.string(), canonicalUrl: z.string(), contentHash: z.string(), score: z.number(),
|
|
309
|
+
}).loose()),
|
|
310
|
+
}).loose()),
|
|
311
|
+
}, async ({ query, limit }) => structured('docs.search', await searchDocs(query, limit)));
|
|
103
312
|
server.registerTool('xeer_check', {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
outputSchema: envelopeOutput,
|
|
108
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
109
|
-
}, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['check', resolved], resolved)));
|
|
313
|
+
...toolPolicy('check'),
|
|
314
|
+
outputSchema: commandOutput,
|
|
315
|
+
}, async ({ directory }) => withDirectory(directory, 'check', (resolved) => commandTool('check', ['check', resolved], resolved)));
|
|
110
316
|
server.registerTool('xeer_build', {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
outputSchema: envelopeOutput,
|
|
115
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
116
|
-
}, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['build', resolved], resolved)));
|
|
317
|
+
...toolPolicy('build'),
|
|
318
|
+
outputSchema: commandOutput,
|
|
319
|
+
}, async ({ directory }) => withDirectory(directory, 'build', (resolved) => commandTool('build', ['build', resolved], resolved)));
|
|
117
320
|
server.registerTool('xeer_test', {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'Run the application\'s own tests: build, type-check tests/**/*.test.ts against the generated',
|
|
121
|
-
'contract, boot the verified artifact in workerd with fresh isolated state, run every test, tear',
|
|
122
|
-
'down. This is the convergence check — a green check proves the project compiles, a green test',
|
|
123
|
-
'proves it behaves. Returns { ok, total, passed, failed, cases, diagnostics, events }.',
|
|
124
|
-
'Each failed case carries a Diagnostic-shaped failure with matcher, expected, actual, and the',
|
|
125
|
-
'project-relative test location: XE1904 assertion, XE1905 threw or unexpectedly refused call,',
|
|
126
|
-
'XE1906 timeout. Compiler diagnostics abort the run before any test boots.',
|
|
127
|
-
'Tests call as(\'alice\'|\'bob\'|\'guest\'), so ownership and authorization are directly testable.',
|
|
128
|
-
].join(' '),
|
|
129
|
-
inputSchema: {
|
|
130
|
-
directory: directoryArgument,
|
|
131
|
-
timeoutMilliseconds: z.number().int().min(1_000).max(1_800_000).optional(),
|
|
132
|
-
},
|
|
133
|
-
outputSchema: {
|
|
134
|
-
argv: z.array(z.string()),
|
|
321
|
+
...toolPolicy('test'),
|
|
322
|
+
outputSchema: wrappedOutput(z.object({
|
|
135
323
|
exitCode: z.number().nullable(),
|
|
136
324
|
ok: z.boolean(),
|
|
137
325
|
reason: z.string().optional(),
|
|
@@ -155,75 +343,80 @@ export function createXeerMcpServer() {
|
|
|
155
343
|
type: z.string(),
|
|
156
344
|
data: z.unknown(),
|
|
157
345
|
}).loose()),
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
346
|
+
}).loose()),
|
|
347
|
+
}, async ({ directory, timeoutMilliseconds }) => withDirectory(directory, 'test', async (resolved) => {
|
|
348
|
+
let run;
|
|
349
|
+
try {
|
|
350
|
+
run = await runXeerTests({
|
|
351
|
+
directory: resolved,
|
|
352
|
+
...(timeoutMilliseconds === undefined ? {} : { timeoutMilliseconds }),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (error instanceof XeerCliError)
|
|
357
|
+
return failure('test', 'adapter_unavailable', error.message);
|
|
358
|
+
return failure('test', 'adapter_failure', 'The xeer test adapter failed before producing a result.');
|
|
359
|
+
}
|
|
360
|
+
const { argv: _argv, stderr: _stderr, ...reusable } = run;
|
|
361
|
+
const adapterDiagnostic = run.diagnostics.find((diagnostic) => diagnostic.code === 'XE0000');
|
|
362
|
+
return structured('test', reusable, {
|
|
363
|
+
ok: run.ok,
|
|
364
|
+
...(adapterDiagnostic ? {
|
|
365
|
+
error: { code: 'adapter_failure', message: adapterDiagnostic.message },
|
|
366
|
+
} : {}),
|
|
165
367
|
});
|
|
166
|
-
return structured(run);
|
|
167
368
|
}));
|
|
168
369
|
server.registerTool('xeer_new', {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
+ 'a manifest by hand. result is { directory, name, files }.',
|
|
173
|
-
inputSchema: {
|
|
174
|
-
directory: z.string().describe('Target directory, relative to the server root. Must be empty or absent.'),
|
|
175
|
-
},
|
|
176
|
-
outputSchema: envelopeOutput,
|
|
177
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
178
|
-
}, async ({ directory }) => withDirectory(directory, (resolved) => commandTool(['new', resolved], projectRoot())));
|
|
370
|
+
...toolPolicy('new'),
|
|
371
|
+
outputSchema: commandOutput,
|
|
372
|
+
}, async ({ directory, template }) => withDirectory(directory, 'new', (resolved) => commandTool('new', ['new', resolved, ...(template === undefined ? [] : ['--template', template])], projectRoot())));
|
|
179
373
|
server.registerTool('xeer_doctor', {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
outputSchema:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
374
|
+
...toolPolicy('doctor'),
|
|
375
|
+
outputSchema: commandOutput,
|
|
376
|
+
}, async ({ directory }) => withDirectory(directory, 'doctor', (resolved) => commandTool('doctor', ['doctor', resolved], resolved)));
|
|
377
|
+
server.registerTool('xeer_deploy_preview', {
|
|
378
|
+
...toolPolicy('deploy.preview'),
|
|
379
|
+
outputSchema: commandOutput,
|
|
380
|
+
}, async ({ directory, controlUrl }) => {
|
|
381
|
+
const refusal = controlOriginRefusal(controlUrl);
|
|
382
|
+
if (refusal)
|
|
383
|
+
return failure('deploy.preview', 'url_refused', refusal);
|
|
384
|
+
return withDirectory(directory, 'deploy.preview', (resolved) => commandTool('deploy.preview', ['deploy', resolved, '--environment', 'preview', ...(controlUrl ? ['--control-url', controlUrl] : [])], resolved));
|
|
385
|
+
});
|
|
386
|
+
if (profile === 'operator') {
|
|
387
|
+
server.registerTool('xeer_promote', {
|
|
388
|
+
...toolPolicy('promote.operator'),
|
|
389
|
+
outputSchema: commandOutput,
|
|
390
|
+
}, async ({ directory, receiptId, controlUrl }) => {
|
|
391
|
+
if (!isReviewReceiptId(receiptId)) {
|
|
392
|
+
return failure('promote.operator', 'invalid_review_receipt_id', 'receiptId must be the complete review_… id returned by a preview deployment.');
|
|
393
|
+
}
|
|
394
|
+
const refusal = controlOriginRefusal(controlUrl);
|
|
395
|
+
if (refusal)
|
|
396
|
+
return failure('promote.operator', 'url_refused', refusal);
|
|
397
|
+
return withDirectory(directory, 'promote.operator', (resolved) => commandTool('promote.operator', ['promote', resolved, '--receipt', receiptId,
|
|
398
|
+
...(controlUrl ? ['--control-url', controlUrl] : [])], resolved));
|
|
399
|
+
});
|
|
400
|
+
}
|
|
200
401
|
server.registerTool('xeer_auth_status', {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
210
|
-
}, async ({ controlUrl }) => commandTool(['auth', 'status', ...(controlUrl ? ['--control-url', controlUrl] : [])], projectRoot()));
|
|
402
|
+
...toolPolicy('auth.status'),
|
|
403
|
+
outputSchema: commandOutput,
|
|
404
|
+
}, async ({ controlUrl }) => {
|
|
405
|
+
const refusal = controlOriginRefusal(controlUrl);
|
|
406
|
+
if (refusal)
|
|
407
|
+
return failure('auth.status', 'url_refused', refusal);
|
|
408
|
+
return commandTool('auth.status', ['auth', 'status', ...(controlUrl ? ['--control-url', controlUrl] : [])], projectRoot());
|
|
409
|
+
});
|
|
211
410
|
server.registerTool('xeer_inspect', {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
+ '(manifest), per-table record counts (state), the bounded structured log ring (logs), or '
|
|
215
|
-
+ 'every stored record with the schema that describes it (export). Use the URLs from '
|
|
216
|
-
+ 'xeer_dev_status.preview.',
|
|
217
|
-
inputSchema: {
|
|
218
|
-
previewUrl: z.string().describe('Preview URL from preview.ready, e.g. http://127.0.0.1:43127/.'),
|
|
219
|
-
view: z.enum(['manifest', 'state', 'logs', 'export']).default('manifest'),
|
|
220
|
-
after: z.string().optional().describe('Log cursor, for view: "logs".'),
|
|
221
|
-
},
|
|
222
|
-
outputSchema: envelopeOutput,
|
|
223
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
411
|
+
...toolPolicy('inspect'),
|
|
412
|
+
outputSchema: commandOutput,
|
|
224
413
|
}, async ({ previewUrl, view, after }) => {
|
|
414
|
+
const activeLocalOrigins = devSessions.summaries().flatMap((session) => session.running && session.preview ? [session.preview.url] : []);
|
|
415
|
+
const refusal = inspectorTargetRefusal(previewUrl, activeLocalOrigins);
|
|
416
|
+
if (refusal)
|
|
417
|
+
return failure('inspect', 'url_refused', refusal);
|
|
225
418
|
const command = view === 'manifest' ? 'inspect' : view;
|
|
226
|
-
return commandTool([command, previewUrl, ...(view === 'logs' && after ? ['--after', after] : [])], projectRoot());
|
|
419
|
+
return commandTool('inspect', [command, previewUrl, ...(view === 'logs' && after ? ['--after', after] : [])], projectRoot());
|
|
227
420
|
});
|
|
228
421
|
const sessionOutput = {
|
|
229
422
|
sessionId: z.string(),
|
|
@@ -243,7 +436,6 @@ export function createXeerMcpServer() {
|
|
|
243
436
|
openDiagnostics: z.array(diagnosticSchema),
|
|
244
437
|
exit: z.object({ code: z.number().optional(), reason: z.string().optional() }).loose().optional(),
|
|
245
438
|
droppedEvents: z.number(),
|
|
246
|
-
stderr: z.string().optional(),
|
|
247
439
|
events: z.array(z.object({
|
|
248
440
|
protocol: z.literal('xeer.dev.v0'),
|
|
249
441
|
seq: z.number(),
|
|
@@ -252,24 +444,17 @@ export function createXeerMcpServer() {
|
|
|
252
444
|
data: z.unknown(),
|
|
253
445
|
}).loose()),
|
|
254
446
|
};
|
|
447
|
+
const reusableSession = (value) => {
|
|
448
|
+
const { stderr: _stderr, directory, ...rest } = value;
|
|
449
|
+
return {
|
|
450
|
+
...rest,
|
|
451
|
+
...(typeof directory === 'string' ? { directory: projectPath(directory) } : {}),
|
|
452
|
+
};
|
|
453
|
+
};
|
|
255
454
|
server.registerTool('xeer_dev_start', {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
'Returns when preview.ready arrives (status "ready"), or when the first compile fails',
|
|
260
|
-
'(status "compile_failed", openDiagnostics populated). Port 0 by default, so the real URL is in',
|
|
261
|
-
'preview.ready. Keep the returned cursor and pass it to xeer_dev_status after each edit.',
|
|
262
|
-
'You do not need a dev session to repair diagnostics: xeer_check is the same compiler.',
|
|
263
|
-
].join(' '),
|
|
264
|
-
inputSchema: {
|
|
265
|
-
directory: directoryArgument,
|
|
266
|
-
host: z.string().optional().describe('Defaults to 127.0.0.1.'),
|
|
267
|
-
port: z.number().int().min(0).max(65535).optional().describe('Defaults to 0 (OS-assigned).'),
|
|
268
|
-
timeoutMilliseconds: z.number().int().min(1_000).max(600_000).optional(),
|
|
269
|
-
},
|
|
270
|
-
outputSchema: sessionOutput,
|
|
271
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
272
|
-
}, async ({ directory, host, port, timeoutMilliseconds }) => withDirectory(directory, async (resolved) => {
|
|
455
|
+
...toolPolicy('dev.start'),
|
|
456
|
+
outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
|
|
457
|
+
}, async ({ directory, host, port, timeoutMilliseconds }) => withDirectory(directory, 'dev.start', async (resolved) => {
|
|
273
458
|
let session;
|
|
274
459
|
try {
|
|
275
460
|
session = devSessions.start({
|
|
@@ -279,71 +464,78 @@ export function createXeerMcpServer() {
|
|
|
279
464
|
});
|
|
280
465
|
}
|
|
281
466
|
catch (error) {
|
|
282
|
-
|
|
467
|
+
if (error instanceof XeerCliError)
|
|
468
|
+
return failure('dev.start', 'adapter_unavailable', error.message);
|
|
469
|
+
return failure('dev.start', 'session_refused', error instanceof Error ? error.message : String(error));
|
|
283
470
|
}
|
|
284
|
-
|
|
285
|
-
|
|
471
|
+
try {
|
|
472
|
+
await session.waitForStart(timeoutMilliseconds ?? 180_000);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return failure('dev.start', 'session_start_failed', 'The dev session failed while starting.');
|
|
476
|
+
}
|
|
477
|
+
if (session.status === 'starting') {
|
|
478
|
+
try {
|
|
479
|
+
await session.stop();
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return failure('dev.start', 'session_stop_failed', 'The timed-out dev session could not be stopped.');
|
|
483
|
+
}
|
|
484
|
+
return structured('dev.start', reusableSession(session.read(0)), {
|
|
485
|
+
ok: false,
|
|
486
|
+
error: {
|
|
487
|
+
code: 'session_start_timeout',
|
|
488
|
+
message: `xeer dev did not become ready within ${timeoutMilliseconds ?? 180_000} ms.`,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
const result = reusableSession(session.read(0));
|
|
493
|
+
return structured('dev.start', result, session.status === 'ready' ? {} : {
|
|
494
|
+
ok: false,
|
|
495
|
+
error: { code: 'session_start_failed', message: `xeer dev stopped with status ${session.status}.` },
|
|
496
|
+
});
|
|
286
497
|
}));
|
|
287
498
|
server.registerTool('xeer_dev_status', {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'Read the xeer.dev.v0 events emitted since `cursor`, then return the session summary.',
|
|
291
|
-
'After editing a file, call this with waitMilliseconds set: the CLI coalesces changes behind a',
|
|
292
|
-
'75 ms quiet window, rebuilds, and emits compile.start, any compile.diagnostic, and compile.ready',
|
|
293
|
-
'when the rebuild is accepted. openDiagnostics is the outcome of the latest attempt, so an empty',
|
|
294
|
-
'openDiagnostics with a higher generation means the edit was accepted.',
|
|
295
|
-
].join(' '),
|
|
296
|
-
inputSchema: {
|
|
297
|
-
sessionId: z.string().optional().describe('Optional when only one session exists.'),
|
|
298
|
-
cursor: z.number().int().min(0).default(0).describe('Last seq you have already read.'),
|
|
299
|
-
waitMilliseconds: z.number().int().min(0).max(600_000).default(0)
|
|
300
|
-
.describe('Wait up to this long for new events to arrive and settle.'),
|
|
301
|
-
limit: z.number().int().min(1).max(1_000).default(200),
|
|
302
|
-
},
|
|
303
|
-
outputSchema: sessionOutput,
|
|
304
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
499
|
+
...toolPolicy('dev.status'),
|
|
500
|
+
outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
|
|
305
501
|
}, async ({ sessionId, cursor, waitMilliseconds, limit }) => {
|
|
306
502
|
let session;
|
|
307
503
|
try {
|
|
308
504
|
session = devSessions.get(sessionId);
|
|
309
505
|
}
|
|
310
506
|
catch (error) {
|
|
311
|
-
return failure(error instanceof Error ? error.message : String(error));
|
|
507
|
+
return failure('dev.status', 'session_refused', error instanceof Error ? error.message : String(error));
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
await session.waitForActivity(cursor, waitMilliseconds);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return failure('dev.status', 'session_read_failed', 'The dev session could not be read.');
|
|
312
514
|
}
|
|
313
|
-
|
|
314
|
-
return structured(session.read(cursor, limit));
|
|
515
|
+
return structured('dev.status', reusableSession(session.read(cursor, limit)));
|
|
315
516
|
});
|
|
316
517
|
server.registerTool('xeer_dev_stop', {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
+ 'the local state lease. Always stop sessions you started: the lease is per project and mode, '
|
|
320
|
-
+ 'so a leaked session blocks the next dev run with XE1812.',
|
|
321
|
-
inputSchema: {
|
|
322
|
-
sessionId: z.string().optional().describe('Optional when only one session exists.'),
|
|
323
|
-
cursor: z.number().int().min(0).default(0),
|
|
324
|
-
},
|
|
325
|
-
outputSchema: sessionOutput,
|
|
326
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
518
|
+
...toolPolicy('dev.stop'),
|
|
519
|
+
outputSchema: wrappedOutput(z.object(sessionOutput).loose()),
|
|
327
520
|
}, async ({ sessionId, cursor }) => {
|
|
328
521
|
let session;
|
|
329
522
|
try {
|
|
330
523
|
session = devSessions.get(sessionId);
|
|
331
524
|
}
|
|
332
525
|
catch (error) {
|
|
333
|
-
return failure(error instanceof Error ? error.message : String(error));
|
|
526
|
+
return failure('dev.stop', 'session_refused', error instanceof Error ? error.message : String(error));
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
await session.stop();
|
|
334
530
|
}
|
|
335
|
-
|
|
336
|
-
|
|
531
|
+
catch {
|
|
532
|
+
return failure('dev.stop', 'session_stop_failed', 'The dev session could not be stopped.');
|
|
533
|
+
}
|
|
534
|
+
return structured('dev.stop', reusableSession(session.read(cursor)));
|
|
337
535
|
});
|
|
338
536
|
server.registerTool('xeer_diagnostics', {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
+ 'Omit `code` for the whole generated reference. This is the same catalogue the compiler emits '
|
|
342
|
-
+ 'from, so it cannot drift from the codes you receive.',
|
|
343
|
-
inputSchema: {
|
|
344
|
-
code: z.string().optional().describe('An XE#### code, e.g. XE1202.'),
|
|
345
|
-
},
|
|
346
|
-
outputSchema: {
|
|
537
|
+
...toolPolicy('diagnostics'),
|
|
538
|
+
outputSchema: wrappedOutput(z.object({
|
|
347
539
|
code: z.string().optional(),
|
|
348
540
|
family: z.string().optional(),
|
|
349
541
|
means: z.string().optional(),
|
|
@@ -351,20 +543,19 @@ export function createXeerMcpServer() {
|
|
|
351
543
|
surfaces: z.array(z.string()).optional(),
|
|
352
544
|
codes: z.array(z.string()).optional(),
|
|
353
545
|
reference: z.string().optional(),
|
|
354
|
-
},
|
|
355
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
546
|
+
}).loose()),
|
|
356
547
|
}, async ({ code }) => {
|
|
357
548
|
if (!code) {
|
|
358
|
-
return structured({ codes: [...DIAGNOSTIC_CODES], reference: renderDiagnosticsReference() });
|
|
549
|
+
return structured('diagnostics', { codes: [...DIAGNOSTIC_CODES], reference: renderDiagnosticsReference() });
|
|
359
550
|
}
|
|
360
551
|
const normalized = code.trim().toUpperCase();
|
|
361
552
|
const definition = diagnosticDefinition(normalized);
|
|
362
553
|
if (!definition) {
|
|
363
554
|
const known = Object.keys(DIAGNOSTICS).sort().join(', ');
|
|
364
|
-
return failure(`Unknown diagnostic code: ${normalized}. Known codes: ${known}`);
|
|
555
|
+
return failure('diagnostics', 'unknown_diagnostic', `Unknown diagnostic code: ${normalized}. Known codes: ${known}`);
|
|
365
556
|
}
|
|
366
557
|
const family = DIAGNOSTIC_FAMILIES.find((candidate) => candidate.prefix === definition.prefix);
|
|
367
|
-
return structured({
|
|
558
|
+
return structured('diagnostics', {
|
|
368
559
|
code: definition.code,
|
|
369
560
|
family: family ? `${family.title}: ${family.summary}` : definition.prefix,
|
|
370
561
|
means: definition.means,
|
|
@@ -372,5 +563,5 @@ export function createXeerMcpServer() {
|
|
|
372
563
|
surfaces: [...definition.surfaces],
|
|
373
564
|
});
|
|
374
565
|
});
|
|
375
|
-
return { server, devSessions };
|
|
566
|
+
return { server, devSessions, profile };
|
|
376
567
|
}
|
package/dist/test-run.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function runXeerTests(options) {
|
|
|
93
93
|
code: 'XE0000',
|
|
94
94
|
severity: 'error',
|
|
95
95
|
message: failure
|
|
96
|
-
??
|
|
96
|
+
?? `xeer test --json exited with ${exitCode} and emitted no events.`,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
done({
|
|
@@ -112,7 +112,7 @@ export async function runXeerTests(options) {
|
|
|
112
112
|
...(stderr.trim() ? { stderr: stderr.trim() } : {}),
|
|
113
113
|
});
|
|
114
114
|
};
|
|
115
|
-
child.on('error', (
|
|
116
|
-
child.on('close', (code) => settle(code, timedOut ? `xeer
|
|
115
|
+
child.on('error', () => settle(null, 'Could not run the xeer CLI.'));
|
|
116
|
+
child.on('close', (code) => settle(code, timedOut ? `xeer test --json exceeded ${timeout} ms and was killed.` : undefined));
|
|
117
117
|
});
|
|
118
118
|
}
|
package/dist/xeer-cli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Diagnostic } from '@impetik/xeer-spec';
|
|
2
2
|
/**
|
|
3
3
|
* Every tool in this server shells out to the `xeer` CLI with `--json` and
|
|
4
|
-
*
|
|
4
|
+
* preserves the envelope it printed inside a versioned MCP result.
|
|
5
5
|
*
|
|
6
6
|
* That is deliberate. The CLI's `xeer.command.v0` envelope and its diagnostics
|
|
7
7
|
* are the versioned contract (docs/specs/dev-protocol-v0.md); re-implementing
|
|
@@ -18,7 +18,7 @@ export interface CommandEnvelope {
|
|
|
18
18
|
result?: unknown;
|
|
19
19
|
}
|
|
20
20
|
export interface CommandRun {
|
|
21
|
-
/**
|
|
21
|
+
/** Internal argv after the CLI path. Never include it in an MCP result: it may contain host paths. */
|
|
22
22
|
argv: string[];
|
|
23
23
|
exitCode: number | null;
|
|
24
24
|
envelope: CommandEnvelope;
|
package/dist/xeer-cli.js
CHANGED
|
@@ -19,9 +19,9 @@ export function resolveXeerCli() {
|
|
|
19
19
|
try {
|
|
20
20
|
return resolve(dirname(require.resolve('@impetik/xeer')), 'cli.js');
|
|
21
21
|
}
|
|
22
|
-
catch
|
|
22
|
+
catch {
|
|
23
23
|
throw new XeerCliError('Could not resolve the xeer CLI. Install @impetik/xeer alongside this server, '
|
|
24
|
-
+
|
|
24
|
+
+ 'or set XEER_CLI to its dist/cli.js.');
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
@@ -44,7 +44,7 @@ export async function resolveDirectory(directory) {
|
|
|
44
44
|
const rootReal = await realpath(root).catch(() => root);
|
|
45
45
|
const requestedReal = await realpath(requested).catch(() => requested);
|
|
46
46
|
if (!contains(root, requested) || !contains(rootReal, requestedReal)) {
|
|
47
|
-
throw new XeerCliError(
|
|
47
|
+
throw new XeerCliError('directory must stay inside the configured XEER_MCP_ROOT. '
|
|
48
48
|
+ 'Set XEER_MCP_ROOT when the server must reach another tree.');
|
|
49
49
|
}
|
|
50
50
|
return requested;
|
|
@@ -113,11 +113,11 @@ export async function runXeerCommand(args, options = {}) {
|
|
|
113
113
|
argv,
|
|
114
114
|
exitCode,
|
|
115
115
|
envelope: envelope ?? crashEnvelope(command, failure
|
|
116
|
-
??
|
|
116
|
+
?? `xeer ${command} --json exited with ${exitCode} and printed no envelope.`),
|
|
117
117
|
...(stderr.trim() ? { stderr: stderr.trim() } : {}),
|
|
118
118
|
});
|
|
119
119
|
};
|
|
120
|
-
child.on('error', (
|
|
121
|
-
child.on('close', (code) => settle(code, timedOut ? `xeer ${
|
|
120
|
+
child.on('error', () => settle(null, 'Could not run the xeer CLI.'));
|
|
121
|
+
child.on('close', (code) => settle(code, timedOut ? `xeer ${command} --json exceeded ${timeout} ms and was killed.` : undefined));
|
|
122
122
|
});
|
|
123
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impetik/xeer-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Model Context Protocol server for Xeer: the scaffold, check, dev, build, and deploy loop as agent tools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/impetik/xeer.git",
|
|
10
10
|
"directory": "packages/mcp"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://xeer.run",
|
|
13
13
|
"bugs": "https://github.com/impetik/xeer/issues",
|
|
14
14
|
"keywords": [
|
|
15
15
|
"xeer",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"zod": "^4.0.10",
|
|
46
|
-
"@impetik/xeer": "0.2.
|
|
47
|
-
"@impetik/xeer-spec": "0.2.
|
|
46
|
+
"@impetik/xeer": "0.2.4",
|
|
47
|
+
"@impetik/xeer-spec": "0.2.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^24.1.0"
|