@zeroroot-ai/gibson-mcp 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/ambient.d.ts +7 -0
  4. package/dist/ambient.js +18 -0
  5. package/dist/ask.d.ts +70 -0
  6. package/dist/ask.js +111 -0
  7. package/dist/build.d.ts +83 -0
  8. package/dist/build.js +209 -0
  9. package/dist/cli.d.ts +88 -0
  10. package/dist/cli.js +186 -0
  11. package/dist/config.d.ts +45 -0
  12. package/dist/config.js +54 -0
  13. package/dist/discovery.d.ts +57 -0
  14. package/dist/discovery.js +132 -0
  15. package/dist/flags.d.ts +32 -0
  16. package/dist/flags.js +85 -0
  17. package/dist/generated/tools.d.ts +15 -0
  18. package/dist/generated/tools.js +276 -0
  19. package/dist/helpers/componentize.d.ts +19 -0
  20. package/dist/helpers/componentize.js +106 -0
  21. package/dist/helpers/context.d.ts +19 -0
  22. package/dist/helpers/context.js +19 -0
  23. package/dist/helpers/coverage.d.ts +23 -0
  24. package/dist/helpers/coverage.js +119 -0
  25. package/dist/helpers/delegate.d.ts +4 -0
  26. package/dist/helpers/delegate.js +182 -0
  27. package/dist/helpers/findings.d.ts +24 -0
  28. package/dist/helpers/findings.js +118 -0
  29. package/dist/helpers/index.d.ts +17 -0
  30. package/dist/helpers/index.js +24 -0
  31. package/dist/helpers/knowledge.d.ts +16 -0
  32. package/dist/helpers/knowledge.js +161 -0
  33. package/dist/helpers/tools.d.ts +113 -0
  34. package/dist/helpers/tools.js +80 -0
  35. package/dist/http.d.ts +57 -0
  36. package/dist/http.js +137 -0
  37. package/dist/inbox.d.ts +88 -0
  38. package/dist/inbox.js +176 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.js +25 -0
  41. package/dist/log.d.ts +4 -0
  42. package/dist/log.js +5 -0
  43. package/dist/main.d.ts +2 -0
  44. package/dist/main.js +61 -0
  45. package/dist/mode.d.ts +32 -0
  46. package/dist/mode.js +21 -0
  47. package/dist/registry.d.ts +83 -0
  48. package/dist/registry.js +133 -0
  49. package/dist/resources.d.ts +63 -0
  50. package/dist/resources.js +98 -0
  51. package/dist/rpc.d.ts +80 -0
  52. package/dist/rpc.js +184 -0
  53. package/dist/schema.d.ts +31 -0
  54. package/dist/schema.js +143 -0
  55. package/dist/server.d.ts +22 -0
  56. package/dist/server.js +70 -0
  57. package/dist/session.d.ts +41 -0
  58. package/dist/session.js +170 -0
  59. package/dist/source.d.ts +30 -0
  60. package/dist/source.js +23 -0
  61. package/dist/state.d.ts +29 -0
  62. package/dist/state.js +37 -0
  63. package/dist/tls.d.ts +1 -0
  64. package/dist/tls.js +19 -0
  65. package/dist/tool.d.ts +17 -0
  66. package/dist/tool.js +22 -0
  67. package/dist/tools/connect.d.ts +24 -0
  68. package/dist/tools/connect.js +115 -0
  69. package/dist/tools/result.d.ts +7 -0
  70. package/dist/tools/result.js +16 -0
  71. package/dist/tools/status.d.ts +5 -0
  72. package/dist/tools/status.js +36 -0
  73. package/dist/turn.d.ts +75 -0
  74. package/dist/turn.js +95 -0
  75. package/package.json +58 -0
@@ -0,0 +1,88 @@
1
+ import type { TaskHarness } from "@zeroroot-ai/sdk";
2
+ import type { Principal } from "@zeroroot-ai/sdk/gen/gibson/common/v1/gibson_common_pb.js";
3
+ import type { Input } from "@zeroroot-ai/sdk/gen/gibson/job/v1/job_pb.js";
4
+ import { InputKind, JobState } from "@zeroroot-ai/sdk/gen/gibson/job/v1/job_pb.js";
5
+ import { type Log } from "./log.js";
6
+ /**
7
+ * The member inbox (gibson#1706, decisions 6 and 11).
8
+ *
9
+ * Input to a long-lived member is one daemon-owned inbox, pulled outbound by
10
+ * the sandbox: the member subscribes and the daemon streams messages down
11
+ * it. The sandbox never accepts an inbound connection, so setec `Attach` is
12
+ * not used and no port is opened.
13
+ *
14
+ * The subscription is a lifetime RPC and runs under the base grant. Each
15
+ * message carries the grant of its own dispatch, which the driver then puts
16
+ * in force for that turn (see turn.ts).
17
+ */
18
+ export { InputKind, JobState };
19
+ /** One message from the inbox, flattened from `gibson.job.v1.Input`. */
20
+ export interface JobInput {
21
+ id: string;
22
+ jobId: string;
23
+ message: string;
24
+ /** The task grant of the dispatch that sent this message. */
25
+ grant: string;
26
+ /** Who sent it, as `<kind>:<id>`: a person, an agent, a tool or a mission node. */
27
+ sender: string;
28
+ kind: InputKind;
29
+ /** Unix milliseconds, or 0 when the daemon sent no timestamp. */
30
+ sentAt: number;
31
+ }
32
+ export declare const MISSING_RPC: string;
33
+ /** True when the harness client carries the inbox RPCs. */
34
+ export declare function inboxAvailable(harness: TaskHarness): boolean;
35
+ /** `<kind>:<id>`, e.g. `KIND_USER:u-1`. Empty when the daemon named nobody. */
36
+ export declare function principalName(p: Principal | undefined): string;
37
+ /** Flatten one wire `Input`. */
38
+ export declare function toJobInput(input: Input | undefined): JobInput;
39
+ export interface InboxOptions {
40
+ harness: TaskHarness;
41
+ log?: Log;
42
+ /** Backoff between reconnects, in ms. Doubles up to the cap. */
43
+ minBackoffMs?: number;
44
+ maxBackoffMs?: number;
45
+ signal?: AbortSignal;
46
+ /** Test seam. */
47
+ sleep?: (ms: number) => Promise<void>;
48
+ }
49
+ export declare const MIN_BACKOFF_MS = 500;
50
+ export declare const MAX_BACKOFF_MS = 30000;
51
+ export interface Inbox {
52
+ /**
53
+ * Every message for this member, in order. Reconnects on its own, so a
54
+ * caller writes one `for await` and never handles a dropped stream.
55
+ */
56
+ messages(): AsyncIterable<JobInput>;
57
+ /** Send a message into a job. Returns the input the daemon recorded. */
58
+ send(jobId: string, message: string, kind: InputKind): Promise<JobInput>;
59
+ /** Tell the daemon a job moved to WORKING or WAITING. */
60
+ reportState(jobId: string, state: JobState, claudeSessionId?: string): Promise<void>;
61
+ /** Stop the subscription. */
62
+ stop(): void;
63
+ }
64
+ /**
65
+ * Subscribe to the inbox, reconnecting with backoff.
66
+ *
67
+ * A dropped stream is normal on a long-lived member: the daemon rolls, the
68
+ * edge recycles a connection. Reconnecting is the wrapper's job, because a
69
+ * driver that had to do it would either drop messages or stop pulling.
70
+ */
71
+ export declare function openInbox(opts: InboxOptions): Inbox;
72
+ /** Somewhere an answer can be delivered. {@link AnswerRouter} implements it. */
73
+ export interface AnswerSink {
74
+ /** Take the message as an answer to an open question. */
75
+ offer(input: JobInput): boolean;
76
+ }
77
+ /**
78
+ * Read the inbox once and split it two ways.
79
+ *
80
+ * A message that answers an open `ask` belongs to that question, not to a
81
+ * new turn, so it is consumed here and never reaches the driver. Everything
82
+ * else is queued for the driver, which decides what turn it starts.
83
+ *
84
+ * The pump runs whether or not the driver is iterating. Waiting for the
85
+ * driver to pull would mean an answer only arrives once the driver asked for
86
+ * the next turn, and the turn cannot start until the question is answered.
87
+ */
88
+ export declare function routeAnswers(inbox: Inbox, answers: AnswerSink, log?: Log): Inbox;
package/dist/inbox.js ADDED
@@ -0,0 +1,176 @@
1
+ import { InputKind, JobState } from "@zeroroot-ai/sdk/gen/gibson/job/v1/job_pb.js";
2
+ import { TAG } from "./log.js";
3
+ /**
4
+ * The member inbox (gibson#1706, decisions 6 and 11).
5
+ *
6
+ * Input to a long-lived member is one daemon-owned inbox, pulled outbound by
7
+ * the sandbox: the member subscribes and the daemon streams messages down
8
+ * it. The sandbox never accepts an inbound connection, so setec `Attach` is
9
+ * not used and no port is opened.
10
+ *
11
+ * The subscription is a lifetime RPC and runs under the base grant. Each
12
+ * message carries the grant of its own dispatch, which the driver then puts
13
+ * in force for that turn (see turn.ts).
14
+ */
15
+ export { InputKind, JobState };
16
+ export const MISSING_RPC = "this daemon's harness has no SubscribeInput. The inbox RPCs ship with gibson.job.v1 " +
17
+ "(buf.build/zeroroot-ai/sdk v0.177.0); update the daemon.";
18
+ /** True when the harness client carries the inbox RPCs. */
19
+ export function inboxAvailable(harness) {
20
+ const client = harness.client;
21
+ return typeof client.subscribeInput === "function" && typeof client.sendInput === "function" && typeof client.reportJobState === "function";
22
+ }
23
+ function inboxClient(harness) {
24
+ if (!inboxAvailable(harness))
25
+ throw new Error(`gibson-mcp: ${MISSING_RPC}`);
26
+ return harness.client;
27
+ }
28
+ /** `<kind>:<id>`, e.g. `KIND_USER:u-1`. Empty when the daemon named nobody. */
29
+ export function principalName(p) {
30
+ if (!p)
31
+ return "";
32
+ return `${p.kind}:${p.id}`;
33
+ }
34
+ /** Flatten one wire `Input`. */
35
+ export function toJobInput(input) {
36
+ return {
37
+ id: input?.id ?? "",
38
+ jobId: input?.jobId ?? "",
39
+ message: input?.message ?? "",
40
+ grant: input?.grant ?? "",
41
+ sender: principalName(input?.sender),
42
+ kind: input?.kind ?? InputKind.UNSPECIFIED,
43
+ sentAt: input?.sentAt ? Number(input.sentAt.seconds) * 1000 : 0,
44
+ };
45
+ }
46
+ export const MIN_BACKOFF_MS = 500;
47
+ export const MAX_BACKOFF_MS = 30_000;
48
+ /**
49
+ * Subscribe to the inbox, reconnecting with backoff.
50
+ *
51
+ * A dropped stream is normal on a long-lived member: the daemon rolls, the
52
+ * edge recycles a connection. Reconnecting is the wrapper's job, because a
53
+ * driver that had to do it would either drop messages or stop pulling.
54
+ */
55
+ export function openInbox(opts) {
56
+ const { harness } = opts;
57
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
58
+ const minBackoff = opts.minBackoffMs ?? MIN_BACKOFF_MS;
59
+ const maxBackoff = opts.maxBackoffMs ?? MAX_BACKOFF_MS;
60
+ let stopped = false;
61
+ const controller = new AbortController();
62
+ opts.signal?.addEventListener("abort", () => controller.abort(), { once: true });
63
+ async function* messages() {
64
+ // Resolved once, outside the retry loop. A daemon with no SubscribeInput
65
+ // is a permanent condition, and retrying it every thirty seconds forever
66
+ // would bury the one line that says what is missing.
67
+ const client = inboxClient(harness);
68
+ let backoff = minBackoff;
69
+ while (!stopped && !controller.signal.aborted) {
70
+ try {
71
+ for await (const res of client.subscribeInput({ context: harness.context }, { signal: controller.signal })) {
72
+ if (stopped)
73
+ return;
74
+ backoff = minBackoff; // a message proves the stream is healthy
75
+ yield toJobInput(res.input);
76
+ }
77
+ // A clean end of stream is the daemon closing the subscription. Ask
78
+ // again after a pause rather than treating it as the end of the
79
+ // member's life.
80
+ }
81
+ catch (e) {
82
+ if (stopped || controller.signal.aborted)
83
+ return;
84
+ opts.log?.(`${TAG} inbox: ${e.message}; reconnecting in ${backoff}ms`);
85
+ }
86
+ if (stopped || controller.signal.aborted)
87
+ return;
88
+ await sleep(backoff);
89
+ backoff = Math.min(maxBackoff, backoff * 2);
90
+ }
91
+ }
92
+ return {
93
+ messages,
94
+ send: async (jobId, message, kind) => {
95
+ const res = await inboxClient(harness).sendInput({ context: harness.context, jobId, message, kind });
96
+ if (res.error)
97
+ throw new Error(`SendInput refused: ${res.error.message}`);
98
+ return toJobInput(res.input);
99
+ },
100
+ reportState: async (jobId, state, claudeSessionId) => {
101
+ const res = await inboxClient(harness).reportJobState({ context: harness.context, jobId, state, claudeSessionId: claudeSessionId ?? "" });
102
+ if (res.error)
103
+ throw new Error(`ReportJobState refused: ${res.error.message}`);
104
+ },
105
+ stop: () => {
106
+ stopped = true;
107
+ controller.abort();
108
+ },
109
+ };
110
+ }
111
+ /**
112
+ * Read the inbox once and split it two ways.
113
+ *
114
+ * A message that answers an open `ask` belongs to that question, not to a
115
+ * new turn, so it is consumed here and never reaches the driver. Everything
116
+ * else is queued for the driver, which decides what turn it starts.
117
+ *
118
+ * The pump runs whether or not the driver is iterating. Waiting for the
119
+ * driver to pull would mean an answer only arrives once the driver asked for
120
+ * the next turn, and the turn cannot start until the question is answered.
121
+ */
122
+ export function routeAnswers(inbox, answers, log) {
123
+ const queue = [];
124
+ const waiting = [];
125
+ let stopped = false;
126
+ const push = (input) => {
127
+ const next = waiting.shift();
128
+ if (next)
129
+ next(input);
130
+ else
131
+ queue.push(input);
132
+ };
133
+ void (async () => {
134
+ try {
135
+ for await (const input of inbox.messages()) {
136
+ if (stopped)
137
+ return;
138
+ if (answers.offer(input))
139
+ continue;
140
+ push(input);
141
+ }
142
+ }
143
+ catch (e) {
144
+ if (!stopped)
145
+ log?.(`${TAG} inbox pump stopped: ${e.message}`);
146
+ }
147
+ })();
148
+ async function* messages() {
149
+ while (!stopped) {
150
+ const first = queue.shift();
151
+ if (first) {
152
+ yield first;
153
+ continue;
154
+ }
155
+ const next = await new Promise((resolve) => {
156
+ waiting.push(resolve);
157
+ if (stopped)
158
+ resolve(undefined);
159
+ });
160
+ if (!next)
161
+ return;
162
+ yield next;
163
+ }
164
+ }
165
+ return {
166
+ messages,
167
+ send: (jobId, message, kind) => inbox.send(jobId, message, kind),
168
+ reportState: (jobId, state, claudeSessionId) => inbox.reportState(jobId, state, claudeSessionId),
169
+ stop: () => {
170
+ stopped = true;
171
+ for (const resolve of waiting.splice(0))
172
+ resolve(undefined);
173
+ inbox.stop();
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,23 @@
1
+ export { buildSurface, type BuildDeps, type Surface } from "./build.js";
2
+ export { parseFlags, parseListen, usage, DEFAULT_LISTEN, DEFAULT_STREAM_LIMIT, type Flags, type Listen, type TransportKind } from "./flags.js";
3
+ export { serveHttp, type HttpHandle, type HttpSurface, type TurnRoute } from "./http.js";
4
+ export { createTurnController, dynamicTransport, TURN_GRANT_HEADER, type Turn, type TurnController } from "./turn.js";
5
+ export { openInbox, routeAnswers, inboxAvailable, toJobInput, principalName, InputKind, JobState, MIN_BACKOFF_MS, MAX_BACKOFF_MS, type Inbox, type JobInput, type AnswerSink, } from "./inbox.js";
6
+ export { askTool, decisionFrom, AnswerRouter, type AnswerSource, type PermissionDecision } from "./ask.js";
7
+ export { attachServer, packageVersion, SERVER_NAME } from "./server.js";
8
+ export { ToolRegistry, type JsonSchema, type ToolContext, type ToolDefinition, type ToolGroup, type ToolHandler } from "./registry.js";
9
+ export { defineTool, jsonSchemaOf, type ToolSpec } from "./tool.js";
10
+ export { openGibson, type Gibson, type OpenGibsonOptions } from "./session.js";
11
+ export { rpcTools, snake, toolNameFor, transportFor, type RpcChannels, type RpcToolOptions } from "./rpc.js";
12
+ export { messageSchema, requestSchema, MAX_DEPTH } from "./schema.js";
13
+ export { startDiscovery, toolKey, pluginKey, DISCOVERY_INTERVAL_MS, type Discovery, type DiscoveryOutcome } from "./discovery.js";
14
+ export { helperTools, helperToolsFor, helperContext, HELPER_TOOL_FOR_EXPORT, NOT_A_TOOL, TOOL_WITHOUT_EXPORT, type HelperContext } from "./helpers/index.js";
15
+ export { GENERATED_SERVICES, GENERATED_RPC_COUNT } from "./generated/tools.js";
16
+ export { decideSource, type CheckInSource, type SourceDecision, type SourceInputs } from "./source.js";
17
+ export { decideMode, type Mode, type ModeDecision } from "./mode.js";
18
+ export { loadSettings, readConfig, writeConfig, resolveSettings, DEFAULT_AGENT_NAME, type Settings, type ServerConfig } from "./config.js";
19
+ export { stateDir, readAmbient, readLive, writeAmbient, writeLive, clearLive, type LiveState } from "./state.js";
20
+ export { describeGibson } from "./tools/status.js";
21
+ export { ambientPrompt, ambientSource, resources, sessionCoordinates, AMBIENT_URI, SESSION_URI, DEFAULT_AMBIENT_QUERY, type AmbientSource, type PromptDefinition, type ResourceDefinition, } from "./resources.js";
22
+ export { ambientBlock } from "./ambient.js";
23
+ export { text, failure, json } from "./tools/result.js";
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ // @zeroroot-ai/gibson-mcp: the Gibson MCP server as a library. The bin is
2
+ // `gibson-mcp` (main.ts); a host adapter or a member driver embeds this.
3
+ export { buildSurface } from "./build.js";
4
+ export { parseFlags, parseListen, usage, DEFAULT_LISTEN, DEFAULT_STREAM_LIMIT } from "./flags.js";
5
+ export { serveHttp } from "./http.js";
6
+ export { createTurnController, dynamicTransport, TURN_GRANT_HEADER } from "./turn.js";
7
+ export { openInbox, routeAnswers, inboxAvailable, toJobInput, principalName, InputKind, JobState, MIN_BACKOFF_MS, MAX_BACKOFF_MS, } from "./inbox.js";
8
+ export { askTool, decisionFrom, AnswerRouter } from "./ask.js";
9
+ export { attachServer, packageVersion, SERVER_NAME } from "./server.js";
10
+ export { ToolRegistry } from "./registry.js";
11
+ export { defineTool, jsonSchemaOf } from "./tool.js";
12
+ export { openGibson } from "./session.js";
13
+ export { rpcTools, snake, toolNameFor, transportFor } from "./rpc.js";
14
+ export { messageSchema, requestSchema, MAX_DEPTH } from "./schema.js";
15
+ export { startDiscovery, toolKey, pluginKey, DISCOVERY_INTERVAL_MS } from "./discovery.js";
16
+ export { helperTools, helperToolsFor, helperContext, HELPER_TOOL_FOR_EXPORT, NOT_A_TOOL, TOOL_WITHOUT_EXPORT } from "./helpers/index.js";
17
+ export { GENERATED_SERVICES, GENERATED_RPC_COUNT } from "./generated/tools.js";
18
+ export { decideSource } from "./source.js";
19
+ export { decideMode } from "./mode.js";
20
+ export { loadSettings, readConfig, writeConfig, resolveSettings, DEFAULT_AGENT_NAME } from "./config.js";
21
+ export { stateDir, readAmbient, readLive, writeAmbient, writeLive, clearLive } from "./state.js";
22
+ export { describeGibson } from "./tools/status.js";
23
+ export { ambientPrompt, ambientSource, resources, sessionCoordinates, AMBIENT_URI, SESSION_URI, DEFAULT_AMBIENT_QUERY, } from "./resources.js";
24
+ export { ambientBlock } from "./ambient.js";
25
+ export { text, failure, json } from "./tools/result.js";
package/dist/log.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** The server logs to stderr only: stdout is the stdio MCP channel. */
2
+ export declare const TAG = "[gibson-mcp]";
3
+ export type Log = (line: string) => void;
4
+ export declare const log: Log;
package/dist/log.js ADDED
@@ -0,0 +1,5 @@
1
+ /** The server logs to stderr only: stdout is the stdio MCP channel. */
2
+ export const TAG = "[gibson-mcp]";
3
+ export const log = (line) => {
4
+ process.stderr.write(`${line}\n`);
5
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { buildSurface } from "./build.js";
4
+ import { parseFlags, usage } from "./flags.js";
5
+ import { serveHttp } from "./http.js";
6
+ import { log, TAG } from "./log.js";
7
+ import { packageVersion } from "./server.js";
8
+ import { describeGibson } from "./tools/status.js";
9
+ /**
10
+ * The bin. This file runs main() unconditionally: npm installs bins as
11
+ * symlinks named after the bin (`gibson-mcp`), so any "am I the entry point"
12
+ * check on argv[1] is false there and the process would exit before it
13
+ * serves anything. Library code lives in build.ts.
14
+ */
15
+ async function main() {
16
+ let flags;
17
+ try {
18
+ flags = parseFlags(process.argv.slice(2));
19
+ }
20
+ catch (e) {
21
+ process.stderr.write(`${TAG} ${e.message}\n\n${usage()}\n`);
22
+ process.exit(2);
23
+ }
24
+ if (flags.help) {
25
+ process.stdout.write(`${usage()}\n`);
26
+ return;
27
+ }
28
+ if (flags.version) {
29
+ process.stdout.write(`${packageVersion()}\n`);
30
+ return;
31
+ }
32
+ const cwd = process.cwd();
33
+ const surface = await buildSurface(process.env, cwd, { streamLimit: flags.streamLimit });
34
+ log(`${TAG} ${describeGibson(surface.current()).replaceAll("\n", "; ")}`);
35
+ log(`${TAG} ${surface.registry.size()} tool(s) registered`);
36
+ let closing = false;
37
+ const close = async (extra) => {
38
+ if (closing)
39
+ return;
40
+ closing = true;
41
+ if (extra)
42
+ await extra().catch((e) => log(`${TAG} close: ${e.message}`));
43
+ await surface.close().catch((e) => log(`${TAG} close: ${e.message}`));
44
+ };
45
+ if (flags.transport === "stdio") {
46
+ // The host ends the session by closing stdio. The mission ends with it.
47
+ process.stdin.on("close", () => void close().finally(() => process.exit(0)));
48
+ for (const sig of ["SIGINT", "SIGTERM"])
49
+ process.on(sig, () => void close().finally(() => process.exit(0)));
50
+ await surface.attach(new StdioServerTransport());
51
+ return;
52
+ }
53
+ const http = await serveHttp(surface, flags.listen, log);
54
+ log(`${TAG} listening on ${http.url}${surface.turn ? `; POST ${http.url.replace(/\/mcp$/, "/turn")} sets the per-turn grant` : ""}`);
55
+ for (const sig of ["SIGINT", "SIGTERM"])
56
+ process.on(sig, () => void close(http.close).finally(() => process.exit(0)));
57
+ }
58
+ main().catch((e) => {
59
+ log(`${TAG} fatal: ${e.message}`);
60
+ process.exit(1);
61
+ });
package/dist/mode.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Which posture this process runs in. Pure: decided from what the
3
+ * environment says and whether the host key exists, so it is testable and the
4
+ * reasons are printed once at start instead of discovered per tool call.
5
+ *
6
+ * - `standalone`: no platform. Findings go to a local log, componentize works.
7
+ * - `component`: checked in, no live mission. Reads and findings run under the
8
+ * component grant. No memory writes: a memory is a World observation and
9
+ * the World is written under a mission (gibson ADR-0012).
10
+ * - `live`: checked in, and this session is a mission (gibson#1593, decision 9).
11
+ * - `task`: a dispatched run (gibson ADR-0016). The launch injected the
12
+ * per-dispatch grant; there is no check-in and no live mission to create,
13
+ * the mission already exists and this run is one of its nodes. Reads and
14
+ * writes use that grant.
15
+ *
16
+ * The postures are one path with fewer tools at each step down, the same
17
+ * fail-open shape as the opencode plugin: a coding agent that cannot reach its
18
+ * platform is still a working coding agent.
19
+ */
20
+ export type Mode = "standalone" | "component" | "live" | "task";
21
+ export interface ModeDecision {
22
+ mode: Mode;
23
+ /** One line a person can act on. Empty when nothing is missing. */
24
+ reason: string;
25
+ }
26
+ export interface ModeInputs {
27
+ platformURL?: string;
28
+ bootstrapToken?: string;
29
+ hostKeyExists: boolean;
30
+ targetId?: string;
31
+ }
32
+ export declare function decideMode(env: ModeInputs, hostKeyPath: string): ModeDecision;
package/dist/mode.js ADDED
@@ -0,0 +1,21 @@
1
+ export function decideMode(env, hostKeyPath) {
2
+ if (!env.platformURL) {
3
+ return { mode: "standalone", reason: "GIBSON_PLATFORM_URL is not set. Call gibson_login, then gibson_connect." };
4
+ }
5
+ if (!env.hostKeyExists && !env.bootstrapToken) {
6
+ return {
7
+ mode: "standalone",
8
+ reason: "GIBSON_PLATFORM_URL is set but this host has not checked in. Call gibson_login then gibson_connect, " +
9
+ "or run `gibson login` and `gibson agent enroll` and start once with GIBSON_BOOTSTRAP_TOKEN=<one-time token>. " +
10
+ `After that the host key at ${hostKeyPath} is enough.`,
11
+ };
12
+ }
13
+ if (!env.targetId) {
14
+ return {
15
+ mode: "component",
16
+ reason: "GIBSON_TARGET_ID is not set, so this session cannot be a live mission and cannot " +
17
+ "write memories. Create a target once (`gibson target create`) and set GIBSON_TARGET_ID, or call gibson_connect with create_target.",
18
+ };
19
+ }
20
+ return { mode: "live", reason: "" };
21
+ }
@@ -0,0 +1,83 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ /** A JSON Schema object for a tool's input. MCP requires `type: "object"` at the top. */
3
+ export interface JsonSchema {
4
+ type: "object";
5
+ [key: string]: unknown;
6
+ }
7
+ /** What a tool call carries besides its arguments. */
8
+ export interface ToolContext {
9
+ /** Request headers of the transport, when it has any (the HTTP transport). */
10
+ headers?: Record<string, string | string[] | undefined>;
11
+ signal?: AbortSignal;
12
+ }
13
+ export type ToolHandler = (args: Record<string, unknown>, ctx: ToolContext) => Promise<CallToolResult>;
14
+ /**
15
+ * Wraps every tool call. The per-turn grant uses one: it reads the request's
16
+ * headers and runs the call under that turn's credential, so a tool built
17
+ * once needs no knowledge of which grant is in force.
18
+ */
19
+ export type ToolMiddleware = (ctx: ToolContext, next: () => Promise<CallToolResult>) => Promise<CallToolResult>;
20
+ export interface ToolAnnotations {
21
+ title?: string;
22
+ readOnlyHint?: boolean;
23
+ destructiveHint?: boolean;
24
+ idempotentHint?: boolean;
25
+ openWorldHint?: boolean;
26
+ }
27
+ export interface ToolDefinition {
28
+ name: string;
29
+ description: string;
30
+ inputSchema: JsonSchema;
31
+ handler: ToolHandler;
32
+ annotations?: ToolAnnotations;
33
+ }
34
+ /**
35
+ * A set of tools that leaves together: one posture, discovery included. A
36
+ * tool registered through a group is dropped by `clear()`, so an upgrade
37
+ * cannot leave a tool behind that points at the previous connection.
38
+ */
39
+ export interface ToolGroup {
40
+ register(def: ToolDefinition): void;
41
+ /** Remove one tool this group registered. */
42
+ remove(name: string): boolean;
43
+ /** Whether ANY tool holds this name, in this group or not. */
44
+ has(name: string): boolean;
45
+ /** Coalesce the changes inside `fn` into one notification. */
46
+ batch<T>(fn: () => T): T;
47
+ /** Remove every tool this group registered. */
48
+ clear(): void;
49
+ names(): string[];
50
+ }
51
+ /** MCP tool names: letters, digits, underscore, hyphen, at most 64 characters. */
52
+ export declare const TOOL_NAME: RegExp;
53
+ /**
54
+ * The one tool table every attached MCP server reads. A transport session
55
+ * gets its own protocol `Server`, and all of them list and call the same
56
+ * tools, so a posture change or a discovery pass reaches every session at
57
+ * once through `tools/list_changed`.
58
+ */
59
+ export declare class ToolRegistry {
60
+ private readonly tools;
61
+ private readonly listeners;
62
+ private readonly middleware;
63
+ private batching;
64
+ private dirty;
65
+ register(def: ToolDefinition): void;
66
+ remove(name: string): boolean;
67
+ has(name: string): boolean;
68
+ get(name: string): ToolDefinition | undefined;
69
+ /** Every tool, sorted by name so a listing is stable. */
70
+ list(): ToolDefinition[];
71
+ size(): number;
72
+ /** Add a wrapper around every tool call. Applied in the order added. */
73
+ use(fn: ToolMiddleware): void;
74
+ call(name: string, args: Record<string, unknown>, ctx: ToolContext): Promise<CallToolResult>;
75
+ group(): ToolGroup;
76
+ /** Coalesce every change inside `fn` into one notification. */
77
+ batch<T>(fn: () => T): T;
78
+ /** Same as {@link batch}, for async work. */
79
+ batchAsync<T>(fn: () => Promise<T>): Promise<T>;
80
+ onChange(fn: () => void): () => void;
81
+ private changed;
82
+ private notify;
83
+ }
@@ -0,0 +1,133 @@
1
+ import { failure } from "./tools/result.js";
2
+ /** MCP tool names: letters, digits, underscore, hyphen, at most 64 characters. */
3
+ export const TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
4
+ /**
5
+ * The one tool table every attached MCP server reads. A transport session
6
+ * gets its own protocol `Server`, and all of them list and call the same
7
+ * tools, so a posture change or a discovery pass reaches every session at
8
+ * once through `tools/list_changed`.
9
+ */
10
+ export class ToolRegistry {
11
+ tools = new Map();
12
+ listeners = new Set();
13
+ middleware = [];
14
+ batching = 0;
15
+ dirty = false;
16
+ register(def) {
17
+ if (!TOOL_NAME.test(def.name))
18
+ throw new Error(`tool name ${JSON.stringify(def.name)} is not [A-Za-z0-9_-]{1,64}`);
19
+ if (this.tools.has(def.name))
20
+ throw new Error(`tool ${def.name} is already registered`);
21
+ if (!def.description.trim())
22
+ throw new Error(`tool ${def.name} has no description`);
23
+ this.tools.set(def.name, def);
24
+ this.changed();
25
+ }
26
+ remove(name) {
27
+ const had = this.tools.delete(name);
28
+ if (had)
29
+ this.changed();
30
+ return had;
31
+ }
32
+ has(name) {
33
+ return this.tools.has(name);
34
+ }
35
+ get(name) {
36
+ return this.tools.get(name);
37
+ }
38
+ /** Every tool, sorted by name so a listing is stable. */
39
+ list() {
40
+ return [...this.tools.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
41
+ }
42
+ size() {
43
+ return this.tools.size;
44
+ }
45
+ /** Add a wrapper around every tool call. Applied in the order added. */
46
+ use(fn) {
47
+ this.middleware.push(fn);
48
+ }
49
+ async call(name, args, ctx) {
50
+ const def = this.tools.get(name);
51
+ if (!def)
52
+ return failure("unknown tool", `No tool named ${name}. Call tools/list for the current set.`);
53
+ let run = () => def.handler(args, ctx);
54
+ for (let i = this.middleware.length - 1; i >= 0; i -= 1) {
55
+ const fn = this.middleware[i];
56
+ const inner = run;
57
+ run = () => fn(ctx, inner);
58
+ }
59
+ try {
60
+ return await run();
61
+ }
62
+ catch (e) {
63
+ return failure(`${name} failed`, e.message);
64
+ }
65
+ }
66
+ group() {
67
+ const names = new Set();
68
+ return {
69
+ register: (def) => {
70
+ this.register(def);
71
+ names.add(def.name);
72
+ },
73
+ remove: (name) => {
74
+ if (!names.delete(name))
75
+ return false;
76
+ return this.remove(name);
77
+ },
78
+ has: (name) => this.has(name),
79
+ batch: (fn) => this.batch(fn),
80
+ clear: () => {
81
+ this.batch(() => {
82
+ for (const n of names)
83
+ this.remove(n);
84
+ names.clear();
85
+ });
86
+ },
87
+ names: () => [...names],
88
+ };
89
+ }
90
+ /** Coalesce every change inside `fn` into one notification. */
91
+ batch(fn) {
92
+ this.batching += 1;
93
+ try {
94
+ return fn();
95
+ }
96
+ finally {
97
+ this.batching -= 1;
98
+ if (this.batching === 0 && this.dirty) {
99
+ this.dirty = false;
100
+ this.notify();
101
+ }
102
+ }
103
+ }
104
+ /** Same as {@link batch}, for async work. */
105
+ async batchAsync(fn) {
106
+ this.batching += 1;
107
+ try {
108
+ return await fn();
109
+ }
110
+ finally {
111
+ this.batching -= 1;
112
+ if (this.batching === 0 && this.dirty) {
113
+ this.dirty = false;
114
+ this.notify();
115
+ }
116
+ }
117
+ }
118
+ onChange(fn) {
119
+ this.listeners.add(fn);
120
+ return () => this.listeners.delete(fn);
121
+ }
122
+ changed() {
123
+ if (this.batching > 0) {
124
+ this.dirty = true;
125
+ return;
126
+ }
127
+ this.notify();
128
+ }
129
+ notify() {
130
+ for (const fn of this.listeners)
131
+ fn();
132
+ }
133
+ }