@irisrun/channel-mcp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/server.js +31 -31
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @irisrun/channel-mcp
|
|
2
|
+
|
|
3
|
+
**Durable, replay-safe sessions — the agent exposed *as* an MCP server.** A minimal,
|
|
4
|
+
faithful Model Context Protocol server over JSON-RPC 2.0 (stdio): `initialize` /
|
|
5
|
+
`tools/list` / `tools/call {start, message}`. It speaks the same two-identifier
|
|
6
|
+
protocol as every Iris channel — a stable `sessionId` plus a single-use
|
|
7
|
+
`continuationToken` rotated every committed turn — and surfaces every failure as a
|
|
8
|
+
**loud** JSON-RPC error, never a silent OK.
|
|
9
|
+
|
|
10
|
+
## What it is
|
|
11
|
+
|
|
12
|
+
`makeMcpChannel(...)` exposes `handle(req)` (the testable core) and `serve(in, out)`
|
|
13
|
+
(newline-delimited JSON-RPC over stdin/stdout). It is **built on
|
|
14
|
+
[`@irisrun/channel-core`](../channel-core/README.md)** — the shared channel **port** —
|
|
15
|
+
so the token discipline (mint, rotate-only-on-a-committed-turn, atomic single-use) and
|
|
16
|
+
the refusal taxonomy live in one place, and this transport just maps refusals to the
|
|
17
|
+
JSON-RPC error range (`-32001` unknown session · `-32002` missing token · `-32003`
|
|
18
|
+
stale token · `-32004` in-flight). It passes the same channel-port conformance suite
|
|
19
|
+
`@irisrun/channel-rest` does — "channels behind one port" is an executed guarantee.
|
|
20
|
+
|
|
21
|
+
Host-side (`node:crypto`); the pure core stays unchanged. MCP is dual-use; a
|
|
22
|
+
real MCP stdio client is an env-gated smoke test.
|
|
23
|
+
|
|
24
|
+
## Use it
|
|
25
|
+
|
|
26
|
+
A client calls the `start` tool to begin a session and `message` to continue it,
|
|
27
|
+
presenting the issued `continuationToken` each turn.
|
|
28
|
+
|
|
29
|
+
See **[docs/Channels](../../docs/channels.md)** and the normative
|
|
30
|
+
**[channel-port spec](../../docs/reference/channel-port-spec.md)**.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
Part of [Iris](../../README.md) — own, portable, verifiable state.
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// makeMcpChannel (
|
|
1
|
+
// makeMcpChannel (MCP is dual-use): the agent exposed AS an MCP server
|
|
2
2
|
// over JSON-RPC 2.0. `handle(req)` is the testable core; `serve(in,out)` frames
|
|
3
3
|
// newline-delimited JSON-RPC over a stream (stdin/stdout in production). It speaks
|
|
4
4
|
// the SAME two-identifier protocol as channel-rest — the channel MINTS the
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// JSON-RPC error, never a silent OK. Host-side (node:crypto); core stays pure.
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { runTurnOn } from "@irisrun/host";
|
|
11
|
+
import { makeChannelSession } from "@irisrun/channel-core";
|
|
11
12
|
// JSON-RPC + MCP error codes. Protocol errors use the standard codes; app errors
|
|
12
13
|
// (token/session) use the implementation-defined -32000 range — all LOUD.
|
|
13
14
|
const PARSE_ERROR = -32700;
|
|
@@ -34,11 +35,17 @@ const MESSAGE_TOOL = {
|
|
|
34
35
|
},
|
|
35
36
|
};
|
|
36
37
|
export function makeMcpChannel(opts) {
|
|
37
|
-
const mintSessionId = opts.mintSessionId ?? (() => randomUUID());
|
|
38
|
-
const mintToken = opts.mintToken ?? (() => randomUUID());
|
|
39
38
|
const serverInfo = opts.serverInfo ?? { name: "iris", version: "0.0.0" };
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// The two-identifier protocol (mint sessionId, own/rotate a single-use token, atomic
|
|
40
|
+
// single-use, committed-only rotation) lives in the shared channel-core port — the
|
|
41
|
+
// SAME driver channel-rest uses. This MCP transport just maps refusals to JSON-RPC
|
|
42
|
+
// error codes. (Previously this file rotated the token on EVERY committed-or-not turn;
|
|
43
|
+
// channel-core corrects that to rotate only on finished/parked.)
|
|
44
|
+
const session = makeChannelSession({
|
|
45
|
+
runTurn: async (sessionId, args) => runTurnOn(opts.adapter, { sessionId, ...opts.makeTurnInputs(sessionId, args) }),
|
|
46
|
+
mintSessionId: opts.mintSessionId ?? (() => randomUUID()),
|
|
47
|
+
mintToken: opts.mintToken ?? (() => randomUUID()),
|
|
48
|
+
});
|
|
42
49
|
const ok = (id, result) => ({ jsonrpc: "2.0", id, result });
|
|
43
50
|
const err = (id, code, message) => ({
|
|
44
51
|
jsonrpc: "2.0",
|
|
@@ -46,7 +53,6 @@ export function makeMcpChannel(opts) {
|
|
|
46
53
|
error: { code, message },
|
|
47
54
|
});
|
|
48
55
|
const toolResult = (payload) => ({ content: [{ type: "text", text: JSON.stringify(payload) }] });
|
|
49
|
-
const runOne = async (sessionId, args) => runTurnOn(opts.adapter, { sessionId, ...opts.makeTurnInputs(sessionId, args) });
|
|
50
56
|
const turnPayload = (sessionId, token, outcome) => {
|
|
51
57
|
const base = { sessionId, continuationToken: token, status: outcome.status };
|
|
52
58
|
if (outcome.status === "finished" && outcome.output !== undefined)
|
|
@@ -57,36 +63,30 @@ export function makeMcpChannel(opts) {
|
|
|
57
63
|
base.current = outcome.current;
|
|
58
64
|
return base;
|
|
59
65
|
};
|
|
66
|
+
// Map a channel-core refusal to its LOUD JSON-RPC error (the impl-defined -32000 range).
|
|
67
|
+
const refusalError = (id, reason, sessionId) => {
|
|
68
|
+
switch (reason) {
|
|
69
|
+
case "unknown-session":
|
|
70
|
+
return err(id, UNKNOWN_SESSION, `unknown session '${sessionId}'`);
|
|
71
|
+
case "missing-token":
|
|
72
|
+
return err(id, MISSING_TOKEN, "missing continuationToken");
|
|
73
|
+
case "stale-token":
|
|
74
|
+
return err(id, STALE_TOKEN, "stale or invalid continuationToken");
|
|
75
|
+
case "in-flight":
|
|
76
|
+
return err(id, IN_FLIGHT, "a turn is already in flight for this session");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
60
79
|
const callStart = async (id, args) => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const token = mintToken();
|
|
64
|
-
tokens.set(sessionId, token);
|
|
65
|
-
return ok(id, toolResult(turnPayload(sessionId, token, outcome)));
|
|
80
|
+
const r = await session.start(args);
|
|
81
|
+
return ok(id, toolResult(turnPayload(r.sessionId, r.token, r.outcome)));
|
|
66
82
|
};
|
|
67
83
|
const callMessage = async (id, args) => {
|
|
68
84
|
const sessionId = typeof args.sessionId === "string" ? args.sessionId : "";
|
|
69
|
-
if (!sessionId || !tokens.has(sessionId))
|
|
70
|
-
return err(id, UNKNOWN_SESSION, `unknown session '${sessionId}'`);
|
|
71
85
|
const presented = typeof args.continuationToken === "string" ? args.continuationToken : null;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// ATOMIC single-use: the token check above and this claim run in ONE callback
|
|
77
|
-
// with no `await` between them; a concurrent second message is refused here.
|
|
78
|
-
if (inFlight.has(sessionId))
|
|
79
|
-
return err(id, IN_FLIGHT, "a turn is already in flight for this session");
|
|
80
|
-
inFlight.add(sessionId);
|
|
81
|
-
try {
|
|
82
|
-
const outcome = await runOne(sessionId, args);
|
|
83
|
-
const next = mintToken();
|
|
84
|
-
tokens.set(sessionId, next); // rotate ONLY after a committed turn → single-use
|
|
85
|
-
return ok(id, toolResult(turnPayload(sessionId, next, outcome)));
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
inFlight.delete(sessionId);
|
|
89
|
-
}
|
|
86
|
+
const r = await session.continueTurn(sessionId, presented, args);
|
|
87
|
+
if (!r.ok)
|
|
88
|
+
return refusalError(id, r.reason, sessionId);
|
|
89
|
+
return ok(id, toolResult(turnPayload(r.sessionId, r.token, r.outcome)));
|
|
90
90
|
};
|
|
91
91
|
const handle = async (raw) => {
|
|
92
92
|
const req = raw;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irisrun/channel-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Iris MCP-server channel (
|
|
5
|
+
"description": "Iris MCP-server channel (MCP dual-use) — the agent exposed AS an MCP server over JSON-RPC 2.0 (stdio). Same two-identifier protocol as channel-rest: the channel mints the sessionId, issues + rotates the continuationToken (atomically single-use), and surfaces every failure as a loud JSON-RPC error. A real MCP stdio client is a manual smoke.",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"iris-src": "./src/index.ts",
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@irisrun/core": "^0.
|
|
15
|
-
"@irisrun/
|
|
14
|
+
"@irisrun/core": "^0.3.0",
|
|
15
|
+
"@irisrun/channel-core": "^0.3.0",
|
|
16
|
+
"@irisrun/host": "^0.3.0"
|
|
16
17
|
},
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"engines": {
|