@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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/ambient.d.ts +7 -0
- package/dist/ambient.js +18 -0
- package/dist/ask.d.ts +70 -0
- package/dist/ask.js +111 -0
- package/dist/build.d.ts +83 -0
- package/dist/build.js +209 -0
- package/dist/cli.d.ts +88 -0
- package/dist/cli.js +186 -0
- package/dist/config.d.ts +45 -0
- package/dist/config.js +54 -0
- package/dist/discovery.d.ts +57 -0
- package/dist/discovery.js +132 -0
- package/dist/flags.d.ts +32 -0
- package/dist/flags.js +85 -0
- package/dist/generated/tools.d.ts +15 -0
- package/dist/generated/tools.js +276 -0
- package/dist/helpers/componentize.d.ts +19 -0
- package/dist/helpers/componentize.js +106 -0
- package/dist/helpers/context.d.ts +19 -0
- package/dist/helpers/context.js +19 -0
- package/dist/helpers/coverage.d.ts +23 -0
- package/dist/helpers/coverage.js +119 -0
- package/dist/helpers/delegate.d.ts +4 -0
- package/dist/helpers/delegate.js +182 -0
- package/dist/helpers/findings.d.ts +24 -0
- package/dist/helpers/findings.js +118 -0
- package/dist/helpers/index.d.ts +17 -0
- package/dist/helpers/index.js +24 -0
- package/dist/helpers/knowledge.d.ts +16 -0
- package/dist/helpers/knowledge.js +161 -0
- package/dist/helpers/tools.d.ts +113 -0
- package/dist/helpers/tools.js +80 -0
- package/dist/http.d.ts +57 -0
- package/dist/http.js +137 -0
- package/dist/inbox.d.ts +88 -0
- package/dist/inbox.js +176 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +25 -0
- package/dist/log.d.ts +4 -0
- package/dist/log.js +5 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +61 -0
- package/dist/mode.d.ts +32 -0
- package/dist/mode.js +21 -0
- package/dist/registry.d.ts +83 -0
- package/dist/registry.js +133 -0
- package/dist/resources.d.ts +63 -0
- package/dist/resources.js +98 -0
- package/dist/rpc.d.ts +80 -0
- package/dist/rpc.js +184 -0
- package/dist/schema.d.ts +31 -0
- package/dist/schema.js +143 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +70 -0
- package/dist/session.d.ts +41 -0
- package/dist/session.js +170 -0
- package/dist/source.d.ts +30 -0
- package/dist/source.js +23 -0
- package/dist/state.d.ts +29 -0
- package/dist/state.js +37 -0
- package/dist/tls.d.ts +1 -0
- package/dist/tls.js +19 -0
- package/dist/tool.d.ts +17 -0
- package/dist/tool.js +22 -0
- package/dist/tools/connect.d.ts +24 -0
- package/dist/tools/connect.js +115 -0
- package/dist/tools/result.d.ts +7 -0
- package/dist/tools/result.js +16 -0
- package/dist/tools/status.d.ts +5 -0
- package/dist/tools/status.js +36 -0
- package/dist/turn.d.ts +75 -0
- package/dist/turn.js +95 -0
- package/package.json +58 -0
package/dist/inbox.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
package/dist/log.js
ADDED
package/dist/main.d.ts
ADDED
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
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -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
|
+
}
|