@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.
- package/LICENSE +21 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +15 -0
- package/dist/chunk-VFNFDDNZ.js +1122 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +26 -0
- package/package.json +32 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|