@irtio/mcp 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ApiClient } from '@irtio/cli/api';
3
+
4
+ /**
5
+ * D39: how an agent authenticates to the MCP server.
6
+ *
7
+ * Resolution order is the credential file `irtio login` writes, then `IRTIO_TOKEN` in the
8
+ * environment. When neither exists the first tool call returns a narrated instruction rather than
9
+ * an auth error, which is handled one layer up in `narrate.ts`; this module's job is only to say
10
+ * honestly which of the three states we are in.
11
+ *
12
+ * **A credential is never read from, or written to, this server's own config file.** The published
13
+ * config snippet contains a command and no secret. That is the whole reason the resolution order
14
+ * starts at the file the CLI already owns: the secret lives in one place, written once by a browser
15
+ * login, mode 0600, and the MCP server borrows it rather than being handed a copy.
16
+ *
17
+ * `IRTIO_TOKEN` is second rather than first because the file is the path a human actually takes.
18
+ * The env var exists for headless cases (CI, a container, this package's own end-to-end tests)
19
+ * where no browser login can happen.
20
+ */
21
+
22
+ /** Why there is no usable credential. The two cases read differently to a human, so they are kept
23
+ * apart even though the narration for both is "run `irtio login`". */
24
+ type AuthFailure = 'no-credential' | 'expired';
25
+ interface AuthResolved {
26
+ readonly ok: true;
27
+ readonly controlUrl: string;
28
+ readonly token: string;
29
+ /** Where the token came from. Reported by `whoami` so an operator can tell which one is in play. */
30
+ readonly source: 'credential-file' | 'env';
31
+ /** Present only when the credential file supplied it, and only ever the file's own record. */
32
+ readonly email: string | undefined;
33
+ }
34
+ interface AuthMissing {
35
+ readonly ok: false;
36
+ readonly controlUrl: string;
37
+ readonly reason: AuthFailure;
38
+ }
39
+ type AuthState = AuthResolved | AuthMissing;
40
+ /** Environment seam, so tests do not have to mutate `process.env`. */
41
+ interface AuthEnv {
42
+ readonly IRTIO_TOKEN?: string | undefined;
43
+ }
44
+ /**
45
+ * Resolves the control URL and a token, in D39's order.
46
+ *
47
+ * An **expired** file credential does not fall through to the env var. If somebody's login has
48
+ * expired, the fix is to log in again, and silently switching to a different identity would make
49
+ * every later tool result describe an account the caller did not think they were using.
50
+ */
51
+ declare function resolveAuth(env?: AuthEnv, controlUrlFlag?: string): Promise<AuthState>;
52
+ /** Thrown when there is no usable credential. `narrate.ts` turns it into the D39 instruction. */
53
+ declare class NoCredentialError extends Error {
54
+ readonly controlUrl: string;
55
+ readonly reason: AuthFailure;
56
+ readonly name = "NoCredentialError";
57
+ constructor(controlUrl: string, reason: AuthFailure);
58
+ }
59
+ /** Resolves auth and builds the client, or throws `NoCredentialError`. */
60
+ declare function requireClient(env?: AuthEnv, controlUrlFlag?: string): Promise<{
61
+ client: ApiClient;
62
+ auth: AuthResolved;
63
+ }>;
64
+
65
+ /**
66
+ * The MCP server itself: a tool registry, an auth resolution, and a narration layer, wired to the
67
+ * SDK's low-level `Server`.
68
+ *
69
+ * The low-level `Server` is used rather than the higher-level helper because the tool schemas here
70
+ * are plain JSON Schema. Going through the helper would mean authoring every schema in zod and
71
+ * taking a second runtime dependency to describe data this package never validates itself, since
72
+ * the control plane is the authority on every argument's meaning.
73
+ *
74
+ * One thing worth stating because it is easy to get wrong: this process speaks the MCP protocol on
75
+ * **stdout**. Nothing may print there. `console.error` is available for diagnostics and the deploy
76
+ * tool captures the CLI's own log lines into its result instead of letting them through.
77
+ */
78
+
79
+ interface McpServerOptions {
80
+ /** Environment the D39 `IRTIO_TOKEN` fallback reads. Defaults to the real one. */
81
+ readonly env?: AuthEnv;
82
+ /** Control URL override. Normally left alone so the credential file decides. */
83
+ readonly controlUrl?: string;
84
+ /** Working directory the `deploy` tool bundles from. Defaults to the process's. */
85
+ readonly cwd?: string;
86
+ }
87
+ declare const SERVER_NAME = "irtio";
88
+ declare const SERVER_VERSION = "0.5.2";
89
+ /**
90
+ * Builds the server. It does not connect: the caller supplies a transport, which is what lets the
91
+ * contract tests drive it in process over a linked pair rather than over a real pipe.
92
+ */
93
+ declare function createMcpServer(options?: McpServerOptions): Server;
94
+
95
+ /**
96
+ * The narration layer, which is one function rather than one per tool (part 2 plan §1.4).
97
+ *
98
+ * D40's contract is that every tool result is prose plus structured data and **ends with the next
99
+ * action**. That is an acceptance criterion, not a nicety: a tool result is read by a model with a
100
+ * finite context window, and the difference between "here is a JSON blob" and "here is what
101
+ * happened and here is what to do about it" is the difference between an agent that takes the next
102
+ * step and one that guesses.
103
+ *
104
+ * Keeping the shape in a single formatter is what stops nineteen tools drifting into nineteen
105
+ * tones. A tool hands over three things — a summary sentence, the structured payload, and the next
106
+ * action — and this module owns everything else: the ordering, the "next:" prefix, and the way an
107
+ * error becomes a narrated result rather than a stack trace.
108
+ *
109
+ * Style rule (m3-plan working rules): tool output is user-facing copy. No em dashes, no LLM tells.
110
+ */
111
+ /** The three things a tool decides. Everything else about the result shape is decided here. */
112
+ interface Narration {
113
+ /** One or more sentences of prose. What happened, in the terms the caller asked in. */
114
+ readonly summary: string;
115
+ /** The machine-readable payload. Always an object, so the MCP `structuredContent` field fits. */
116
+ readonly data: Record<string, unknown>;
117
+ /** The next action, without the "next:" prefix. A command, or a named tool, or both. */
118
+ readonly next: string;
119
+ }
120
+ /** The MCP tool result shape, narrowed to what this server produces. */
121
+ interface ToolResult {
122
+ readonly content: readonly {
123
+ readonly type: 'text';
124
+ readonly text: string;
125
+ }[];
126
+ readonly structuredContent: Record<string, unknown>;
127
+ readonly isError?: boolean;
128
+ }
129
+ /**
130
+ * The one place the result shape is decided: prose, then the payload as pretty JSON, then the next
131
+ * action. The JSON is repeated inside the text block on purpose. `structuredContent` is the field a
132
+ * client should read, but not every client surfaces it to the model, and a tool whose numbers are
133
+ * invisible half the time is worse than a tool that repeats itself.
134
+ */
135
+ declare function narrate(n: Narration): ToolResult;
136
+ /** The D39 login narration. Deliberately an instruction, not an error. */
137
+ declare const LOGIN_NARRATION: string;
138
+ /**
139
+ * Maps a thrown error to a narrated result. Central, so the auth story and the deploy 409 read the
140
+ * same way through every tool that can raise them.
141
+ *
142
+ * The cases that earn their own wording:
143
+ *
144
+ * - **401 and `NotLoggedInError`** become the D39 instruction. An agent that reads "unauthorized"
145
+ * invents a fix; an agent that reads "run `irtio login`" runs it.
146
+ * - **409 `E_BREAKING_SCHEMA`** relays the server's own `changes` and `hint` verbatim. Control
147
+ * classifies breaking versus additive authoritatively, and restating its verdict in other words
148
+ * would be a second implementation of the thing D40 says not to reimplement.
149
+ * - **404 on a project route** says "not found in your organisation", because the API deliberately
150
+ * answers 404 rather than 403 across orgs and this server must not leak the distinction back.
151
+ */
152
+ declare function narrateError(err: unknown, toolName: string): ToolResult;
153
+
154
+ /**
155
+ * The tool surface (part 2 plan §1.3, D40).
156
+ *
157
+ * Thin, flat, `verb_noun`, one tool per intent. Every tool is a narrated client call: it takes an
158
+ * `ApiClient`, hits exactly one control-plane route (deploy is the documented exception, and it
159
+ * borrows the CLI's handshake rather than writing a second one), and hands `narrate.ts` a summary,
160
+ * a payload and a next action.
161
+ *
162
+ * Two rules the descriptions follow, because a tool description is the only documentation an agent
163
+ * reliably reads:
164
+ *
165
+ * 1. **Say what it changes.** A reader deciding between `project_get` and `project_update` needs to
166
+ * know which one writes before it calls one.
167
+ * 2. **Say what it costs.** `deploy` bundles and uploads and can drain live rooms; `logs` is a
168
+ * cheap read. Those are not the same kind of call and the description says so.
169
+ *
170
+ * What is deliberately absent: anything that mutates outside the project (org settings, billing,
171
+ * fleet) is out of the surface entirely per D40, and there is no tool that reads a stored secret.
172
+ * `jwt_secret_mint` returns a plaintext secret exactly once because that is the only moment it
173
+ * exists to be returned; there is no route that reads one back and there must not be one.
174
+ */
175
+
176
+ /**
177
+ * What a tool is handed. `cwd` matters to `deploy` and `scenario_run`, the two tools that read
178
+ * local files.
179
+ *
180
+ * `client` is optional because one tool does not need it. `scenario_run` runs bots against a local
181
+ * dev room and never speaks to the control plane, and failing a local verification run because a
182
+ * credential expired would be the wrong answer to the right question. Every other tool reads it
183
+ * through `api()`, which turns an absent client into the D39 login narration.
184
+ */
185
+ interface ToolContext {
186
+ readonly client: ApiClient | undefined;
187
+ readonly controlUrl: string;
188
+ readonly cwd: string;
189
+ }
190
+ /** A JSON Schema object, kept loose: the MCP wire format wants plain JSON Schema, not a zod type. */
191
+ type JsonSchema = Record<string, unknown>;
192
+ interface ToolDef {
193
+ readonly name: string;
194
+ /**
195
+ * D41: this tool needs no control-plane credential. `scenario_run` runs bots against a local
196
+ * dev room, and a local verification must not fail because a login expired.
197
+ */
198
+ readonly local?: boolean;
199
+ readonly description: string;
200
+ readonly inputSchema: JsonSchema;
201
+ readonly run: (ctx: ToolContext, args: Record<string, unknown>) => Promise<Narration>;
202
+ }
203
+ /** The registry, read-only tools first, then mutations, then deploy (plan §3). */
204
+ declare const TOOLS: readonly ToolDef[];
205
+ declare function toolByName(name: string): ToolDef | undefined;
206
+
207
+ export { type AuthEnv, type AuthMissing, type AuthResolved, type AuthState, LOGIN_NARRATION, type McpServerOptions, type Narration, NoCredentialError, SERVER_NAME, SERVER_VERSION, TOOLS, type ToolContext, type ToolDef, type ToolResult, createMcpServer, narrate, narrateError, requireClient, resolveAuth, toolByName };
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import {
2
+ LOGIN_NARRATION,
3
+ NoCredentialError,
4
+ SERVER_NAME,
5
+ SERVER_VERSION,
6
+ TOOLS,
7
+ createMcpServer,
8
+ narrate,
9
+ narrateError,
10
+ requireClient,
11
+ resolveAuth,
12
+ toolByName
13
+ } from "./chunk-VFNFDDNZ.js";
14
+ export {
15
+ LOGIN_NARRATION,
16
+ NoCredentialError,
17
+ SERVER_NAME,
18
+ SERVER_VERSION,
19
+ TOOLS,
20
+ createMcpServer,
21
+ narrate,
22
+ narrateError,
23
+ requireClient,
24
+ resolveAuth,
25
+ toolByName
26
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@irtio/mcp",
3
+ "version": "0.6.0",
4
+ "description": "irtio MCP server: provision, deploy, observe and restore irtio rooms from a coding agent",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "irtio-mcp": "./dist/bin.js"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "1.30.0",
26
+ "@irtio/cli": "0.6.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "test": "vitest run"
31
+ }
32
+ }