@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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/server.js +31 -31
  3. 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 (ADR-0009, MCP is dual-use): the agent exposed AS an MCP server
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
- const tokens = new Map(); // the channel OWNS the current token per session
41
- const inFlight = new Set(); // single-use guard under concurrency (see channel-rest)
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 sessionId = mintSessionId();
62
- const outcome = await runOne(sessionId, args);
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
- if (presented === null || presented === "")
73
- return err(id, MISSING_TOKEN, "missing continuationToken");
74
- if (presented !== tokens.get(sessionId))
75
- return err(id, STALE_TOKEN, "stale or invalid continuationToken");
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.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
- "description": "Iris MCP-server channel (ADR-0009, 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.",
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.1.0",
15
- "@irisrun/host": "^0.1.0"
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": {