toolroll 0.9.42 → 0.9.44

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 (68) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/action-ledger.d.ts +5 -2
  3. package/dist/action-ledger.js +13 -3
  4. package/dist/chat-actions.js +1 -1
  5. package/dist/chat-approval.d.ts +45 -0
  6. package/dist/chat-approval.js +81 -0
  7. package/dist/chat-channel.js +5 -0
  8. package/dist/chat-decide.d.ts +18 -6
  9. package/dist/chat-decide.js +54 -20
  10. package/dist/chat-flow.js +2 -2
  11. package/dist/cli-http.d.ts +64 -0
  12. package/dist/cli-http.js +143 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.js +8 -1
  15. package/dist/contracts/cli.d.ts +13 -0
  16. package/dist/contracts/cli.js +7 -0
  17. package/dist/contracts/console-api.d.ts +1 -0
  18. package/dist/contracts/console-api.js +1 -0
  19. package/dist/contracts/gateway-tools.d.ts +82 -0
  20. package/dist/contracts/gateway-tools.js +33 -0
  21. package/dist/contracts/lead-tools.d.ts +11 -0
  22. package/dist/contracts/lead-tools.js +2 -0
  23. package/dist/desktop-update.d.ts +3 -1
  24. package/dist/desktop-update.js +7 -6
  25. package/dist/flow-engine.d.ts +2 -0
  26. package/dist/flow-engine.js +10 -2
  27. package/dist/flows-cli.d.ts +28 -0
  28. package/dist/flows-cli.js +43 -1
  29. package/dist/guides.js +2 -0
  30. package/dist/mate-contract.js +1 -1
  31. package/dist/mate-tools.js +43 -1
  32. package/dist/mcp-core.d.ts +110 -0
  33. package/dist/mcp-core.js +311 -0
  34. package/dist/mcp-http.d.ts +26 -0
  35. package/dist/mcp-http.js +151 -0
  36. package/dist/mcp-person.d.ts +31 -0
  37. package/dist/mcp-person.js +96 -0
  38. package/dist/mcp.d.ts +1 -8
  39. package/dist/mcp.js +21 -322
  40. package/dist/operate-remote-arguments.d.ts +36 -0
  41. package/dist/operate-remote-arguments.js +185 -0
  42. package/dist/operate-remote.d.ts +86 -0
  43. package/dist/operate-remote.js +125 -0
  44. package/dist/operate.d.ts +21 -1
  45. package/dist/operate.js +229 -15
  46. package/dist/process-recovery-eligibility.d.ts +1 -1
  47. package/dist/remote-command.d.ts +45 -0
  48. package/dist/remote-command.js +124 -0
  49. package/dist/remote-exec.d.ts +18 -0
  50. package/dist/remote-exec.js +241 -0
  51. package/dist/remote-run.d.ts +16 -0
  52. package/dist/remote-run.js +13 -0
  53. package/dist/scope.d.ts +4 -2
  54. package/dist/scope.js +10 -0
  55. package/dist/serve.d.ts +7 -0
  56. package/dist/serve.js +75 -2
  57. package/dist/store.d.ts +23 -2
  58. package/dist/store.js +76 -9
  59. package/dist/surface.d.ts +10 -3
  60. package/dist/surface.js +71 -3
  61. package/dist/team-cli.d.ts +19 -0
  62. package/dist/team-cli.js +20 -3
  63. package/dist/teammate-work.js +1 -1
  64. package/dist/telegram-flow.d.ts +41 -1
  65. package/dist/telegram-flow.js +96 -12
  66. package/dist/telegram.js +27 -3
  67. package/dist/toolroll-update.js +4 -1
  68. package/package.json +1 -1
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The MCP gateway over streamable HTTP, served at /mcp by the console (serve.ts). Stateless: one JSON-RPC message per
3
+ * POST, answered with one JSON body (a notification gets 202 and no body); there is no session and no event stream.
4
+ * The modern revision carries its version in `_meta` as on stdio; a handshake-era client may initialize, and because
5
+ * nothing is kept between requests its tools work without the stdio lifecycle.
6
+ *
7
+ * Every POST signs in again. The caller is a coordinator credential (the coordinator registry, exactly as on stdio) or
8
+ * a person's own API token `so_…` (mcp-person.ts: their role, project grants and token scope). A password
9
+ * (`name:password`) and cookies sign in nothing here, a request carrying a browser Origin is refused, and the console's
10
+ * Host check runs before this route. A revoked or expired token answers 401 on its next request.
11
+ */
12
+ import { PACKAGE_VERSION } from "./version.js";
13
+ import { parseApiToken, secretMatches } from "./api-tokens.js";
14
+ import { authenticateCoordinator } from "./coordinator.js";
15
+ import { LEGACY, MAX_REQUEST, META_CAPABILITIES, META_SERVER, META_VERSION, MODERN, TOOLS, UNSUPPORTED_VERSION, callCoordinatorTool, capabilitiesShape, readMessage, shapeResult, toolsPayload, } from "./mcp-core.js";
16
+ import { callPersonTool, personTools } from "./mcp-person.js";
17
+ /** The most decisions a coordinator credential is remembered to have read (propose_answer needs one). */
18
+ const READ_DECISIONS_KEPT = 200;
19
+ export function createMcpHttp(options) {
20
+ const { store } = options;
21
+ /** Stdio remembers per connection which decisions a coordinator read; stateless HTTP remembers it per credential. */
22
+ const readDecisions = new Map();
23
+ const send = (response, status, body, headers = {}) => {
24
+ response.writeHead(status, { "cache-control": "no-store", "x-content-type-options": "nosniff", ...(body === null ? {} : { "content-type": "application/json" }), ...headers });
25
+ response.end(body === null ? undefined : JSON.stringify(body));
26
+ };
27
+ const refuse = (response, status, message, headers = {}) => send(response, status, { jsonrpc: "2.0", id: null, error: { code: -32000, message } }, headers);
28
+ /** Who signed this request, from the live rows — nothing is cached, so revocation and grant changes apply at once. */
29
+ const callerOf = (request) => {
30
+ const presented = /^Bearer (\S+)$/.exec(request.headers.authorization ?? "")?.[1];
31
+ if (presented === undefined || presented.includes(":"))
32
+ return null;
33
+ if (presented.startsWith("so_")) {
34
+ if (!options.signedIn(request))
35
+ return null;
36
+ const parsed = parseApiToken(presented);
37
+ const kept = parsed === null ? null : store.apiTokenSecret(parsed.id);
38
+ // The secret is checked here too, so this door never rests on how signedIn reads the request.
39
+ if (parsed === null || kept === null || !secretMatches(parsed.secret, kept.secretHash))
40
+ return null;
41
+ if (kept.row.revokedAt !== null || Date.parse(kept.row.expiresAt) <= options.clock().getTime())
42
+ return null;
43
+ const account = store.accountOf(kept.row.account);
44
+ if (account === null || account.revokedAt !== null)
45
+ return null;
46
+ return {
47
+ kind: "person",
48
+ person: {
49
+ principal: { kind: "person", account: kept.row.account, generation: account.generation, scope: kept.row.access, tokenId: kept.row.id, projects: account.projects === null ? null : [...account.projects] },
50
+ role: account.role,
51
+ tokenName: kept.row.name,
52
+ },
53
+ };
54
+ }
55
+ const auth = authenticateCoordinator(store, presented);
56
+ return auth.ok ? { kind: "coordinator", token: presented, cid: auth.who.cid } : null;
57
+ };
58
+ /** Loaded on first use: operate.ts imports serve.ts, which serves this gateway. */
59
+ const runAs = async () => options.runAs ?? (await import("./operate.js")).runOperateAs;
60
+ /** The body, or null the moment it passes MAX_REQUEST: reading stops there and nothing more is buffered. */
61
+ const readBody = (request) => new Promise((resolve, reject) => {
62
+ const chunks = [];
63
+ let size = 0;
64
+ const onData = (chunk) => {
65
+ size += chunk.length;
66
+ if (size <= MAX_REQUEST)
67
+ return void chunks.push(chunk);
68
+ request.off("data", onData);
69
+ request.pause();
70
+ chunks.length = 0;
71
+ resolve(null);
72
+ };
73
+ request.on("data", onData);
74
+ request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
75
+ request.on("error", reject);
76
+ });
77
+ /** 413, then the connection goes: the rest of an oversized body is never read. */
78
+ const tooLarge = (request, response) => {
79
+ response.once("finish", () => request.destroy());
80
+ refuse(response, 413, "request over 256 KiB", { connection: "close" });
81
+ };
82
+ const call = async (caller, params) => {
83
+ if (caller.kind === "person") {
84
+ return callPersonTool(caller.person, store, await runAs(), params);
85
+ }
86
+ let read = readDecisions.get(caller.cid);
87
+ if (read === undefined)
88
+ readDecisions.set(caller.cid, read = new Set());
89
+ const outcome = callCoordinatorTool({ store, token: caller.token, enrolled: options.enrolled(), clock: options.clock, readDecisions: read, ...(options.evidenceRoot === undefined ? {} : { evidenceRoot: options.evidenceRoot }) }, params);
90
+ for (const id of read)
91
+ if (read.size > READ_DECISIONS_KEPT)
92
+ read.delete(id);
93
+ return outcome;
94
+ };
95
+ return async (request, response) => {
96
+ if (request.method !== "POST")
97
+ return refuse(response, 405, "the MCP gateway takes POST only — one JSON-RPC message per request", { allow: "POST" });
98
+ // A browser page must never drive the gateway with whatever it can reach: agents send no Origin.
99
+ if (request.headers.origin !== undefined)
100
+ return refuse(response, 403, "browser requests are refused — connect an MCP client with your API token");
101
+ const caller = callerOf(request);
102
+ if (caller === null) {
103
+ return refuse(response, 401, "sign in with your API token (Authorization: Bearer so_…) or a coordinator credential — passwords and cookies are not accepted here", { "www-authenticate": 'Bearer realm="toolroll-mcp"' });
104
+ }
105
+ if (!/^application\/json\s*(;|$)/i.test(request.headers["content-type"] ?? ""))
106
+ return refuse(response, 415, "send the JSON-RPC message as application/json");
107
+ if (Number(request.headers["content-length"] ?? 0) > MAX_REQUEST)
108
+ return tooLarge(request, response);
109
+ const text = await readBody(request);
110
+ if (text === null)
111
+ return tooLarge(request, response);
112
+ const message = readMessage(text);
113
+ if (!message.ok)
114
+ return send(response, 400, { jsonrpc: "2.0", id: message.id, error: { code: message.code, message: message.message } });
115
+ const { id, method, params, meta } = message;
116
+ const error = (code, text, data) => send(response, 200, { jsonrpc: "2.0", id, error: { code, message: text, ...(data === undefined ? {} : { data }) } });
117
+ const answer = (era, result, cacheable) => send(response, 200, { jsonrpc: "2.0", id, result: shapeResult(era, result, cacheable) });
118
+ if (!store.schemaCurrent())
119
+ return error(-32000, "the database schema moved underneath this server — try again after it restarts");
120
+ // Notifications (initialized, cancelled) need nothing from a stateless server: accepted, never answered.
121
+ if (method.startsWith("notifications/"))
122
+ return send(response, 202, null);
123
+ const header = request.headers["mcp-protocol-version"];
124
+ const declared = typeof meta[META_VERSION] === "string" ? meta[META_VERSION] : typeof header === "string" ? header : null;
125
+ if (method !== "initialize" && declared !== null && declared !== MODERN && declared !== LEGACY) {
126
+ return error(UNSUPPORTED_VERSION, `unsupported protocol version \`${declared}\``, { supported: [MODERN, LEGACY], requested: declared });
127
+ }
128
+ const tools = () => caller.kind === "person" ? personTools(caller.person) : TOOLS;
129
+ if (method === "initialize") {
130
+ // The handshake era negotiates only itself; nothing is kept, so no session id is issued.
131
+ return answer("legacy", { protocolVersion: LEGACY, capabilities: { tools: {} }, serverInfo: { name: "toolroll", version: PACKAGE_VERSION } }, false);
132
+ }
133
+ const era = declared === MODERN ? "modern" : "legacy";
134
+ if (era === "modern" && !capabilitiesShape(meta[META_CAPABILITIES]))
135
+ return error(-32600, `a ${MODERN} request carries _meta["${META_CAPABILITIES}"]`);
136
+ if (method === "server/discover") {
137
+ if (era !== "modern")
138
+ return error(-32600, `server/discover requires _meta["${META_VERSION}"] = "${MODERN}" and _meta["${META_CAPABILITIES}"]`);
139
+ return answer("modern", { protocolVersion: MODERN, supportedVersions: [MODERN, LEGACY], capabilities: { tools: {} }, _meta: { [META_SERVER]: { name: "toolroll", version: PACKAGE_VERSION } }, ...toolsPayload(tools()) }, true);
140
+ }
141
+ if (method === "ping")
142
+ return era === "modern" ? error(-32601, "ping is not part of the modern revision") : answer("legacy", {}, false);
143
+ if (method === "tools/list")
144
+ return answer(era, toolsPayload(tools()), true);
145
+ if (method === "tools/call") {
146
+ const outcome = await call(caller, params);
147
+ return outcome.kind === "result" ? answer(era, outcome.result, false) : error(outcome.code, outcome.message);
148
+ }
149
+ return error(-32601, `unknown method \`${method}\``);
150
+ };
151
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A person's tools on the HTTP gateway. A person signs in with their own API token (api-tokens.ts) and every tool call
3
+ * is the exact `toolroll` command line they could type, run on the server under their principal through
4
+ * `runOperateAs` with source "mcp": the command's own authorization, project grants and attribution apply, and its
5
+ * `--json` envelope is the answer. Nothing here reads data directly — a coordinator handler would skip the person's
6
+ * project grants — and nothing here approves, answers or changes people or policy: those verbs have no tool.
7
+ *
8
+ * tools/list follows the account's role and the token's scope; dispatch checks again, independent of the list.
9
+ */
10
+ import { type PersonToolName } from "./contracts/gateway-tools.js";
11
+ import { type CallOutcome, type Json } from "./mcp-core.js";
12
+ import type { Store } from "./store.js";
13
+ import type { Principal } from "./operate.js";
14
+ /** The shared remote seam (operate.ts): who runs a command on the server, and the runner itself. Type-only, so a
15
+ * change to either breaks this build instead of drifting; the value is loaded lazily (operate.ts imports serve.ts). */
16
+ export type { Principal } from "./operate.js";
17
+ export type RunOperateAs = typeof import("./operate.js").runOperateAs;
18
+ /** A signed-in person: the principal the command runs as, and what their account may do. */
19
+ export type Person = {
20
+ principal: Principal;
21
+ role: "approver" | "viewer";
22
+ tokenName: string;
23
+ };
24
+ /** What this person may call: every read, and the acts only on an act token of an account that acts. */
25
+ export declare function personTools(person: Person): {
26
+ name: PersonToolName;
27
+ description: string;
28
+ inputSchema: Json;
29
+ }[];
30
+ /** One person tools/call: read by its schema, checked against the person, then run as the command it names. */
31
+ export declare function callPersonTool(person: Person, store: Store, runAs: RunOperateAs, params: Record<string, unknown>): Promise<CallOutcome>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * A person's tools on the HTTP gateway. A person signs in with their own API token (api-tokens.ts) and every tool call
3
+ * is the exact `toolroll` command line they could type, run on the server under their principal through
4
+ * `runOperateAs` with source "mcp": the command's own authorization, project grants and attribution apply, and its
5
+ * `--json` envelope is the answer. Nothing here reads data directly — a coordinator handler would skip the person's
6
+ * project grants — and nothing here approves, answers or changes people or policy: those verbs have no tool.
7
+ *
8
+ * tools/list follows the account's role and the token's scope; dispatch checks again, independent of the list.
9
+ */
10
+ import { parseContract, toModelSchema } from "./contracts/contract.js";
11
+ import { PERSON_TOOL_INPUTS, PERSON_TOOL_OUTPUTS } from "./contracts/gateway-tools.js";
12
+ import { reportToolOutput } from "./contracts/lead-tools.js";
13
+ import { callArguments } from "./mcp-core.js";
14
+ const TOOLS = {
15
+ status: {
16
+ description: "Running work, queued reasons, results to review and plan windows, over the projects you may see.",
17
+ acts: false,
18
+ argv: () => ["status"],
19
+ },
20
+ list_tasks: {
21
+ description: "A page of tasks in the projects you may see. Filter by state and repo; cursor-paginated.",
22
+ acts: false,
23
+ argv: args => [
24
+ "task", "list",
25
+ ...(args.state === undefined ? [] : ["--state", args.state]),
26
+ ...(args.repo === undefined ? [] : ["--repo", args.repo]),
27
+ ...(args.cursor === undefined ? [] : ["--cursor", String(args.cursor)]),
28
+ ...(args.limit === undefined ? [] : ["--limit", String(args.limit)]),
29
+ ],
30
+ },
31
+ task_show: {
32
+ description: "One task in full. A task outside your projects answers unknown-task.",
33
+ acts: false,
34
+ argv: args => ["task", "show", args.ref],
35
+ },
36
+ task_review: {
37
+ description: "A task's compact result packet: saved conclusion, changed files, checks, findings, screenshots, plan and next actions. Pass run for an exact finished attempt; all includes LOW findings.",
38
+ acts: false,
39
+ argv: args => ["task", "review", args.ref, "--brief", ...(args.run === undefined ? [] : ["--run", String(args.run)]), ...(args.all === true ? ["--all"] : [])],
40
+ },
41
+ review_findings: {
42
+ description: "One run's automatic review findings.",
43
+ acts: false,
44
+ argv: args => ["task", "review", String(args.run)],
45
+ },
46
+ file_task: {
47
+ description: "File a task into one of your projects, under your name. It waits for approval as usual; approving stays in the console or chat. Replaying the same idempotency_key returns the first answer.",
48
+ acts: true,
49
+ argv: args => ["task", "add", args.title, "--repo", args.repo, "--key", args.idempotency_key, ...(args.deliverable === "report" ? ["--report"] : [])],
50
+ },
51
+ };
52
+ const NAMES = Object.keys(PERSON_TOOL_INPUTS);
53
+ /** What this person may call: every read, and the acts only on an act token of an account that acts. */
54
+ export function personTools(person) {
55
+ return NAMES.filter(name => mayCall(person, name)).map(name => ({ name, description: TOOLS[name].description, inputSchema: toModelSchema(PERSON_TOOL_INPUTS[name]) }));
56
+ }
57
+ const mayCall = (person, name) => !TOOLS[name].acts || person.principal.scope === "act" && person.role === "approver";
58
+ /** One person tools/call: read by its schema, checked against the person, then run as the command it names. */
59
+ export async function callPersonTool(person, store, runAs, params) {
60
+ const name = typeof params["name"] === "string" ? params["name"] : "";
61
+ if (!NAMES.includes(name))
62
+ return { kind: "error", code: -32602, message: `no tool \`${name}\``, fatal: false };
63
+ const tool = name;
64
+ const rawArgs = callArguments(params);
65
+ if (rawArgs === null)
66
+ return { kind: "error", code: -32602, message: "arguments must be an object", fatal: false };
67
+ const read = parseContract(PERSON_TOOL_INPUTS[tool], rawArgs);
68
+ if (!read.ok)
69
+ return { kind: "error", code: -32602, message: read.issues.map(one => one.line).join("; "), fatal: false };
70
+ if (!mayCall(person, tool)) {
71
+ const why = person.principal.scope === "read" ? "this token reads only — make an act token in the console to file work" : "your account can watch, not act";
72
+ return refused(`${tool}: ${why}`);
73
+ }
74
+ const argv = [...TOOLS[tool].argv(read.value), "--json"];
75
+ let written = "";
76
+ let code;
77
+ try {
78
+ code = await runAs(argv, { principal: person.principal, store, write: chunk => { written += chunk; }, source: "mcp" });
79
+ }
80
+ catch (error) {
81
+ return refused(`the command failed on the server: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ const text = written.trim();
84
+ let body;
85
+ try {
86
+ body = JSON.parse(text);
87
+ }
88
+ catch {
89
+ return refused(text === "" ? `the command answered nothing (exit ${code})` : text);
90
+ }
91
+ const ok = code === 0 && typeof body === "object" && body !== null && body["ok"] === true;
92
+ if (ok)
93
+ reportToolOutput("gateway", tool, PERSON_TOOL_OUTPUTS[tool], body);
94
+ return { kind: "result", result: ok ? { content: [{ type: "text", text }] } : { content: [{ type: "text", text }], isError: true } };
95
+ }
96
+ const refused = (message) => ({ kind: "result", result: { content: [{ type: "text", text: message }], isError: true } });
package/dist/mcp.d.ts CHANGED
@@ -1,12 +1,5 @@
1
1
  import type { Store } from "./store.js";
2
- export declare const MODERN = "2026-07-28";
3
- export declare const LEGACY = "2025-11-25";
4
- /** The modern revision namespaces its per-request metadata. */
5
- export declare const META_VERSION = "io.modelcontextprotocol/protocolVersion";
6
- export declare const META_SERVER = "io.modelcontextprotocol/serverInfo";
7
- export declare const META_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
8
- /** The revision's own error code for an unsupported protocol version. */
9
- export declare const UNSUPPORTED_VERSION = -32022;
2
+ export { LEGACY, META_CAPABILITIES, META_SERVER, META_VERSION, MODERN, UNSUPPORTED_VERSION } from "./mcp-core.js";
10
3
  export type McpIo = {
11
4
  onLine: (handler: (line: string) => void) => void;
12
5
  onEof: (handler: () => void) => void;