@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZeroRoot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @zeroroot-ai/gibson-mcp
|
|
2
|
+
|
|
3
|
+
The Gibson MCP server. One tool surface for every coding agent host: Claude
|
|
4
|
+
Code, opencode, Cursor, Codex CLI, Gemini CLI and Windsurf. The host is a thin
|
|
5
|
+
adapter and holds no tools of its own.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Every host runs it through `npx`, so nothing has to be installed by hand.
|
|
10
|
+
This is the canonical block. It is the same for every host that reads the
|
|
11
|
+
`mcpServers` shape.
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"gibson": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["--yes", "--package", "@zeroroot-ai/gibson-mcp@latest", "gibson-mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Host | Where the block goes |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Claude Code | `.mcp.json` in the project, or the `zerocool` plugin, which carries it |
|
|
27
|
+
| opencode | the `zerocool` opencode plugin, which carries it |
|
|
28
|
+
| Cursor | `.cursor/mcp.json` |
|
|
29
|
+
| Codex CLI | `~/.codex/config.toml`, as a `[mcp_servers.gibson]` table with the same command and args |
|
|
30
|
+
| Gemini CLI | `~/.gemini/settings.json`, under `mcpServers` |
|
|
31
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json`, under `mcpServers` |
|
|
32
|
+
|
|
33
|
+
A host is an adapter and holds no tools of its own (ADR-0008). The per-host
|
|
34
|
+
snippets and their smoke tests live in
|
|
35
|
+
[`zerocool-plugins`](https://github.com/zeroroot-ai/zerocool-plugins).
|
|
36
|
+
|
|
37
|
+
## The tool surface
|
|
38
|
+
|
|
39
|
+
The surface is 1:1 with everything the SDK produces. One flat tier, no
|
|
40
|
+
curated subset.
|
|
41
|
+
|
|
42
|
+
| Where a tool comes from | Naming | How many |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| every RPC of every SDK service, generated from the proto descriptors | `<service>_<method>` in snake case, e.g. `harness_callback_service_world_view` | 188 |
|
|
45
|
+
| every SDK helper | the helper's own name: `remember`, `recall`, `world_view`, `submit_finding`, `delegate` | 30 |
|
|
46
|
+
| every checked-in platform tool and plugin, discovered at runtime | `gibson_<tool>`, `gibson_plugin_<plugin>` | whatever the tenant has |
|
|
47
|
+
|
|
48
|
+
An SDK bump regenerates the first group. Discovery repeats every 60 seconds
|
|
49
|
+
and emits `tools/list_changed`, so a tool a person enrols now is callable in
|
|
50
|
+
the same session.
|
|
51
|
+
|
|
52
|
+
A posture registers only the tools its credential can reach. With no
|
|
53
|
+
platform the server still serves `submit_finding`, `componentize` and
|
|
54
|
+
`validate_component`, plus `gibson_login` and `gibson_connect`.
|
|
55
|
+
|
|
56
|
+
## Resources and prompts
|
|
57
|
+
|
|
58
|
+
| URI | What it holds |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `gibson://ambient` | one GraphRAG lookup per session: what the tenant already knows about this codebase |
|
|
61
|
+
| `gibson://session` | the check-in source, the posture, and the mission, run and callback endpoint a session-end hook needs |
|
|
62
|
+
|
|
63
|
+
A host with a hook surface injects the ambient block itself. A host without
|
|
64
|
+
one calls the `gibson_ambient` prompt instead. Either way it is one lookup.
|
|
65
|
+
|
|
66
|
+
## Transports
|
|
67
|
+
|
|
68
|
+
| Flag | Where it runs | Why |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `--transport stdio` (default) | a laptop | the host spawns the server and owns its lifetime |
|
|
71
|
+
| `--transport http --listen 127.0.0.1:7788` | a sandbox | one server for the life of the sandbox, so the driver can swap the grant per turn |
|
|
72
|
+
|
|
73
|
+
`--listen` accepts a loopback address only, and DNS rebinding protection is
|
|
74
|
+
on. `--stream-limit` (default 500) caps how many messages a server-streaming
|
|
75
|
+
RPC tool returns before it reports `truncated`.
|
|
76
|
+
|
|
77
|
+
### The HTTP routes
|
|
78
|
+
|
|
79
|
+
| Route | What it does |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `POST /mcp` | starts an MCP session with `initialize`; later requests carry `mcp-session-id` |
|
|
82
|
+
| `GET /mcp` | the session's notification stream |
|
|
83
|
+
| `DELETE /mcp` | ends a session |
|
|
84
|
+
| `GET /healthz` | liveness, plus the check-in source, the posture, the tool count and the open job |
|
|
85
|
+
| `POST /turn` | puts a dispatch's grant in force |
|
|
86
|
+
| `GET /turn` | reports the turn in force |
|
|
87
|
+
| `DELETE /turn` | ends it |
|
|
88
|
+
|
|
89
|
+
### Per-turn grants
|
|
90
|
+
|
|
91
|
+
A member sandbox serves many dispatches over its life, and each input
|
|
92
|
+
message carries the task grant of its own dispatch. The driver calls
|
|
93
|
+
`POST /turn` before it feeds Claude a message:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
POST /turn
|
|
97
|
+
{"job_id": "job-1", "grant": "<CG-JWT>", "callback_endpoint": "daemon:50001"}
|
|
98
|
+
-> 200 {"job_id": "job-1", "endpoint": "daemon:50001"}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Every tool call that follows runs under that grant. `DELETE /turn` ends the
|
|
102
|
+
turn, and calls fall back to the **base grant** from the launch, which is
|
|
103
|
+
used for the lifetime RPCs only: the inbox subscription, reading repository
|
|
104
|
+
credentials, and the checkpoint writes. A single request may instead carry
|
|
105
|
+
`x-gibson-turn-grant`, which applies to that request alone and wins over the
|
|
106
|
+
turn in force; that is how a driver runs two turns at once.
|
|
107
|
+
|
|
108
|
+
Both `job_id` and `grant` are required. A grant with no job attributes the
|
|
109
|
+
work to nothing.
|
|
110
|
+
|
|
111
|
+
`/turn` exists only where there is a task grant to swap. Elsewhere it is a
|
|
112
|
+
404.
|
|
113
|
+
|
|
114
|
+
## Check-in sources
|
|
115
|
+
|
|
116
|
+
The server picks one credential source at start from what is present. It never
|
|
117
|
+
mixes them, and it never mints identity.
|
|
118
|
+
|
|
119
|
+
1. **Dispatched grant.** `GIBSON_CG_JWT` and `GIBSON_CALLBACK_ENDPOINT` are
|
|
120
|
+
set, so the daemon launched this process. The server joins the run it was
|
|
121
|
+
launched for. No enrollment, no state file, no mission. This source wins
|
|
122
|
+
over every other.
|
|
123
|
+
2. **Pre-minted token.** `GIBSON_BOOTSTRAP_TOKEN` is set and this host has no
|
|
124
|
+
key yet. The server checks in once with the token. The host key carries
|
|
125
|
+
every later start, and the token is spent.
|
|
126
|
+
3. **Enrolled host key.** The key at `GIBSON_HOST_KEY_PATH` (default
|
|
127
|
+
`~/.zerocool/host.key`) is the credential.
|
|
128
|
+
4. **Nothing yet.** The server offers `gibson_login` and `gibson_connect`: a
|
|
129
|
+
person signs in through the `gibson` CLI device flow, and the server
|
|
130
|
+
enrolls the host, picks the target and starts the live mission without a
|
|
131
|
+
restart.
|
|
132
|
+
|
|
133
|
+
Call `gibson_status` to see the source, the posture, the platform, the tenant,
|
|
134
|
+
the target and the mission.
|
|
135
|
+
|
|
136
|
+
## Postures
|
|
137
|
+
|
|
138
|
+
A platform the server cannot reach never stops a session. Each posture below
|
|
139
|
+
carries fewer tools than the one before it.
|
|
140
|
+
|
|
141
|
+
- `task`: a dispatched run. Reads and writes use the dispatch grant.
|
|
142
|
+
- `live`: checked in, and this session is a mission of its own.
|
|
143
|
+
- `component`: checked in, no mission. Reads and findings only.
|
|
144
|
+
- `standalone`: no platform. Findings go to a local log.
|
|
145
|
+
|
|
146
|
+
## Environment
|
|
147
|
+
|
|
148
|
+
| Variable | Meaning |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `GIBSON_PLATFORM_URL` | the platform to check in to |
|
|
151
|
+
| `GIBSON_TARGET_ID` | the target the live mission binds to |
|
|
152
|
+
| `GIBSON_BOOTSTRAP_TOKEN` | a one-time enrollment token |
|
|
153
|
+
| `GIBSON_HOST_KEY_PATH` | the host key (default `~/.zerocool/host.key`) |
|
|
154
|
+
| `GIBSON_CA_CERT` | a private CA to trust |
|
|
155
|
+
| `GIBSON_CALLBACK_INSECURE` | `1` dials the callback endpoint without TLS. Local daemons only. |
|
|
156
|
+
| `ZEROCOOL_STATE_DIR` | the state directory (default `~/.zerocool`) |
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type KnowledgeSource } from "@zeroroot-ai/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* One GraphRAG lookup per session, read by a session-start hook or the gibson://ambient resource. A
|
|
4
|
+
* knowledge failure never breaks a start: the agent works without prior
|
|
5
|
+
* context, it just works less well.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ambientBlock(knowledge: KnowledgeSource, seedQuery: string): Promise<string>;
|
package/dist/ambient.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { formatKnowledgeForPrompt } from "@zeroroot-ai/sdk";
|
|
2
|
+
/** How many hits the ambient block carries. Small: it is prompt overhead on every session. */
|
|
3
|
+
const AMBIENT_LIMIT = 5;
|
|
4
|
+
/**
|
|
5
|
+
* One GraphRAG lookup per session, read by a session-start hook or the gibson://ambient resource. A
|
|
6
|
+
* knowledge failure never breaks a start: the agent works without prior
|
|
7
|
+
* context, it just works less well.
|
|
8
|
+
*/
|
|
9
|
+
export async function ambientBlock(knowledge, seedQuery) {
|
|
10
|
+
try {
|
|
11
|
+
const hits = await knowledge.query({ text: seedQuery, topK: AMBIENT_LIMIT });
|
|
12
|
+
const body = formatKnowledgeForPrompt(hits);
|
|
13
|
+
return body ? `Prior context from the Gibson knowledge graph (recall for more):\n${body}` : "";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/ask.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type Inbox, type JobInput } from "./inbox.js";
|
|
2
|
+
import { type Log } from "./log.js";
|
|
3
|
+
import type { ToolDefinition } from "./registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* `ask`: the one way a job reaches a person (gibson#1706, decision 16).
|
|
6
|
+
*
|
|
7
|
+
* The wire has no `INPUT_KIND_QUESTION`. `gibson.job.v1.InputKind` carries
|
|
8
|
+
* `TURN`, `ANSWER` and `WRAP_UP`, and issue #59 expected a
|
|
9
|
+
* `JobService.SendEvent` that B1 did not ship. So the question is signalled
|
|
10
|
+
* two ways with what exists: `ReportJobState(job_id, WAITING)` is the state
|
|
11
|
+
* the console renders and the stale-limit reaper reads, and the text rides
|
|
12
|
+
* on an input, which is the only field on the wire that carries prose. The
|
|
13
|
+
* id the daemon returns for that input is remembered, so a member that is
|
|
14
|
+
* handed back its own question says so instead of answering itself.
|
|
15
|
+
*
|
|
16
|
+
* A job runs Claude Code with `--dangerously-skip-permissions`; the gVisor
|
|
17
|
+
* sandbox and the per-turn grant are the controls. So there is no permission
|
|
18
|
+
* dialog, and a question has to travel the same path as every other input:
|
|
19
|
+
* out through the job, back through the inbox. The driver wires this tool as
|
|
20
|
+
* `--permission-prompt-tool mcp__gibson__ask`.
|
|
21
|
+
*
|
|
22
|
+
* The job enters `waiting` while the question is outstanding, and the next
|
|
23
|
+
* input for that job is the answer. Input for another job is not an answer
|
|
24
|
+
* to this question, so it is left in the stream for its own turn.
|
|
25
|
+
*
|
|
26
|
+
* The reply is the shape Claude Code's permission-prompt contract expects:
|
|
27
|
+
* `{"behavior":"allow","updatedInput":{...}}` or
|
|
28
|
+
* `{"behavior":"deny","message":"..."}`. A free-text answer to a plain
|
|
29
|
+
* question comes back as an allow carrying the text, because that is how a
|
|
30
|
+
* question that is not about a tool call is answered.
|
|
31
|
+
*/
|
|
32
|
+
/** What Claude Code reads back from a permission-prompt tool. */
|
|
33
|
+
export type PermissionDecision = {
|
|
34
|
+
behavior: "allow";
|
|
35
|
+
updatedInput: Record<string, unknown>;
|
|
36
|
+
} | {
|
|
37
|
+
behavior: "deny";
|
|
38
|
+
message: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function decisionFrom(answer: string, toolInput: Record<string, unknown>): PermissionDecision;
|
|
41
|
+
/** A source of answers: the next input for one job. */
|
|
42
|
+
export interface AnswerSource {
|
|
43
|
+
/** Resolve with the next input for `jobId`, or reject when it cannot come. */
|
|
44
|
+
next(jobId: string, signal?: AbortSignal): Promise<JobInput>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Route inbox messages to whoever is waiting for that job.
|
|
48
|
+
*
|
|
49
|
+
* One reader of the stream, many waiters: two jobs can each hold an open
|
|
50
|
+
* question at the same time, and a message for one must not wake the other.
|
|
51
|
+
*/
|
|
52
|
+
export declare class AnswerRouter implements AnswerSource {
|
|
53
|
+
private readonly waiting;
|
|
54
|
+
private readonly pending;
|
|
55
|
+
/** Offer a message. Returns true when a waiter took it. */
|
|
56
|
+
offer(input: JobInput): boolean;
|
|
57
|
+
/** Hold a message that arrived before anyone asked for it. */
|
|
58
|
+
hold(input: JobInput): void;
|
|
59
|
+
next(jobId: string, signal?: AbortSignal): Promise<JobInput>;
|
|
60
|
+
}
|
|
61
|
+
export interface AskOptions {
|
|
62
|
+
/** The job the current turn belongs to. */
|
|
63
|
+
jobId: () => string | undefined;
|
|
64
|
+
inbox: Pick<Inbox, "send" | "reportState">;
|
|
65
|
+
answers: AnswerSource;
|
|
66
|
+
/** The Claude Code session this job runs, when the driver knows it. */
|
|
67
|
+
claudeSessionId?: () => string | undefined;
|
|
68
|
+
log?: Log;
|
|
69
|
+
}
|
|
70
|
+
export declare function askTool(opts: AskOptions): ToolDefinition;
|
package/dist/ask.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { InputKind, JobState } from "./inbox.js";
|
|
3
|
+
import { TAG } from "./log.js";
|
|
4
|
+
import { defineTool } from "./tool.js";
|
|
5
|
+
import { failure, text } from "./tools/result.js";
|
|
6
|
+
/** Words an answer may use to refuse. Anything else allows. */
|
|
7
|
+
const DENIALS = /^\s*(no|deny|denied|reject|rejected|refuse|refused|stop|do not|don't)\b/i;
|
|
8
|
+
export function decisionFrom(answer, toolInput) {
|
|
9
|
+
if (DENIALS.test(answer))
|
|
10
|
+
return { behavior: "deny", message: answer.trim() };
|
|
11
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Route inbox messages to whoever is waiting for that job.
|
|
15
|
+
*
|
|
16
|
+
* One reader of the stream, many waiters: two jobs can each hold an open
|
|
17
|
+
* question at the same time, and a message for one must not wake the other.
|
|
18
|
+
*/
|
|
19
|
+
export class AnswerRouter {
|
|
20
|
+
waiting = new Map();
|
|
21
|
+
pending = new Map();
|
|
22
|
+
/** Offer a message. Returns true when a waiter took it. */
|
|
23
|
+
offer(input) {
|
|
24
|
+
const queue = this.waiting.get(input.jobId);
|
|
25
|
+
if (!queue)
|
|
26
|
+
return false;
|
|
27
|
+
const resolve = queue.shift();
|
|
28
|
+
if (!resolve)
|
|
29
|
+
return false;
|
|
30
|
+
if (queue.length === 0)
|
|
31
|
+
this.waiting.delete(input.jobId);
|
|
32
|
+
resolve(input);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** Hold a message that arrived before anyone asked for it. */
|
|
36
|
+
hold(input) {
|
|
37
|
+
if (this.offer(input))
|
|
38
|
+
return;
|
|
39
|
+
const queue = this.pending.get(input.jobId) ?? [];
|
|
40
|
+
queue.push(input);
|
|
41
|
+
this.pending.set(input.jobId, queue);
|
|
42
|
+
}
|
|
43
|
+
next(jobId, signal) {
|
|
44
|
+
const held = this.pending.get(jobId);
|
|
45
|
+
const first = held?.shift();
|
|
46
|
+
if (first) {
|
|
47
|
+
if (held && held.length === 0)
|
|
48
|
+
this.pending.delete(jobId);
|
|
49
|
+
return Promise.resolve(first);
|
|
50
|
+
}
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const queue = this.waiting.get(jobId) ?? [];
|
|
53
|
+
queue.push(resolve);
|
|
54
|
+
this.waiting.set(jobId, queue);
|
|
55
|
+
signal?.addEventListener("abort", () => {
|
|
56
|
+
const q = this.waiting.get(jobId);
|
|
57
|
+
const i = q?.indexOf(resolve) ?? -1;
|
|
58
|
+
if (q && i >= 0)
|
|
59
|
+
q.splice(i, 1);
|
|
60
|
+
reject(new Error("the question was cancelled before an answer arrived"));
|
|
61
|
+
}, { once: true });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function askTool(opts) {
|
|
66
|
+
return defineTool({
|
|
67
|
+
name: "ask",
|
|
68
|
+
description: "Ask the person or the agent that opened this job a question, and wait for the answer. Use it " +
|
|
69
|
+
"when you need a decision you cannot make yourself: a choice between two approaches, a missing " +
|
|
70
|
+
"fact, or permission for something outside the job's declared deliverable. The job waits while " +
|
|
71
|
+
"the question is open, so ask once and ask precisely.",
|
|
72
|
+
input: {
|
|
73
|
+
question: z.string().describe("The question, in one or two sentences. Say what you will do with each answer."),
|
|
74
|
+
tool_name: z.string().optional().describe("When this is a permission prompt, the tool Claude Code wants to run."),
|
|
75
|
+
input: z.record(z.string(), z.unknown()).optional().describe("When this is a permission prompt, the input Claude Code wants to run it with."),
|
|
76
|
+
},
|
|
77
|
+
handler: async (args, ctx) => {
|
|
78
|
+
const jobId = opts.jobId();
|
|
79
|
+
if (!jobId) {
|
|
80
|
+
return failure("no job is open", "ask reaches the person through the job's inbox, and this turn belongs to no job. " +
|
|
81
|
+
"The driver sets the job with POST /turn before it feeds a message.");
|
|
82
|
+
}
|
|
83
|
+
const question = args.tool_name ? `${args.question}\n\nClaude Code wants to run ${args.tool_name} with:\n${JSON.stringify(args.input ?? {}, null, 2)}` : args.question;
|
|
84
|
+
try {
|
|
85
|
+
// The wait is armed before the question is sent. Arming it after
|
|
86
|
+
// would drop an answer that came back faster than this call returns.
|
|
87
|
+
const answer = opts.answers.next(jobId, ctx.signal);
|
|
88
|
+
// WAITING is the signal that a question is open: it is what the
|
|
89
|
+
// console renders and what the stale-limit reaper reads. The text
|
|
90
|
+
// rides on an input, because that is the only field on the wire that
|
|
91
|
+
// carries prose.
|
|
92
|
+
await opts.inbox.reportState(jobId, JobState.WAITING, opts.claudeSessionId?.());
|
|
93
|
+
const sent = await opts.inbox.send(jobId, question, InputKind.TURN);
|
|
94
|
+
opts.log?.(`${TAG} job ${jobId} is waiting on a question`);
|
|
95
|
+
const input = await answer.finally(async () => {
|
|
96
|
+
await opts.inbox.reportState(jobId, JobState.WORKING, opts.claudeSessionId?.()).catch(() => { });
|
|
97
|
+
});
|
|
98
|
+
opts.log?.(`${TAG} job ${jobId} got its answer from ${input.sender || "the inbox"}`);
|
|
99
|
+
if (input.id && input.id === sent.id) {
|
|
100
|
+
return failure("ask heard its own question", "The inbox returned the question this job just posted. The daemon must not deliver a member its own input.");
|
|
101
|
+
}
|
|
102
|
+
if (args.tool_name)
|
|
103
|
+
return text("", JSON.stringify(decisionFrom(input.message, args.input ?? {})));
|
|
104
|
+
return text("", input.message);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return failure("ask failed", e.message);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { Transport as ConnectTransport } from "@connectrpc/connect";
|
|
3
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
|
+
import { ToolRegistry, type ToolDefinition, type ToolGroup } from "./registry.js";
|
|
5
|
+
import type { TurnRoute } from "./http.js";
|
|
6
|
+
import { type Discovery } from "./discovery.js";
|
|
7
|
+
import { type Inbox } from "./inbox.js";
|
|
8
|
+
import { type RpcChannels } from "./rpc.js";
|
|
9
|
+
import { type AmbientSource } from "./resources.js";
|
|
10
|
+
import { type Gibson } from "./session.js";
|
|
11
|
+
import { type ConnectDeps } from "./tools/connect.js";
|
|
12
|
+
/**
|
|
13
|
+
* @zeroroot-ai/gibson-mcp: the one MCP server every coding agent host loads
|
|
14
|
+
* (gibson#1706, decisions 1 to 4). It exposes what the SDK produces and adds
|
|
15
|
+
* Gibson to the session: one live mission per session (ADR-0007 decision 3),
|
|
16
|
+
* memory and knowledge, findings, tools, delegation. It never routes LLM
|
|
17
|
+
* traffic and never reads a model provider key.
|
|
18
|
+
*
|
|
19
|
+
* The surface is one tool registry that every attached protocol server
|
|
20
|
+
* reads. `upgrade` swaps the connection (after `gibson_connect`) and
|
|
21
|
+
* re-registers the posture's tools, and every attached session hears
|
|
22
|
+
* `tools/list_changed`.
|
|
23
|
+
*/
|
|
24
|
+
export interface BuildDeps extends ConnectDeps {
|
|
25
|
+
streamLimit?: number;
|
|
26
|
+
/** How often to look for newly checked-in platform tools. `0` runs one pass. */
|
|
27
|
+
discoveryIntervalMs?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface Surface {
|
|
30
|
+
registry: ToolRegistry;
|
|
31
|
+
/** The current connection. Changes on gibson_connect. */
|
|
32
|
+
current(): Gibson;
|
|
33
|
+
/** Replace the connection and re-register the posture's tools. */
|
|
34
|
+
upgrade(next: Gibson): Promise<void>;
|
|
35
|
+
/** Bind a new protocol server to a transport. */
|
|
36
|
+
attach(transport: Transport): Promise<Server>;
|
|
37
|
+
/** Small JSON for /healthz. */
|
|
38
|
+
health(): Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* The per-turn grant control, when this posture holds a task grant. The
|
|
41
|
+
* HTTP transport serves it at `/turn`; the stdio transport does not expose
|
|
42
|
+
* it, because a host that spawns one process per session has one grant.
|
|
43
|
+
*/
|
|
44
|
+
turn?: TurnRoute;
|
|
45
|
+
/** The member inbox, when this posture holds a task grant. */
|
|
46
|
+
inbox?: Inbox;
|
|
47
|
+
/** End the mission, stop the heartbeat, drop the state file. Idempotent. */
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export declare function buildSurface(env: NodeJS.ProcessEnv, cwd: string, deps?: BuildDeps): Promise<Surface>;
|
|
51
|
+
export interface PostureContext {
|
|
52
|
+
env: NodeJS.ProcessEnv;
|
|
53
|
+
cwd: string;
|
|
54
|
+
streamLimit: number;
|
|
55
|
+
/** The session's ambient block, for the hook handoff. */
|
|
56
|
+
ambient?: AmbientSource;
|
|
57
|
+
discoveryIntervalMs?: number;
|
|
58
|
+
/** The per-turn transport, when this posture serves turns. */
|
|
59
|
+
taskTransport?: ConnectTransport;
|
|
60
|
+
/** The `ask` tool, when this posture has an inbox to ask through. */
|
|
61
|
+
ask?: ToolDefinition;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Which transports this posture holds, for the generated RPC tools.
|
|
65
|
+
*
|
|
66
|
+
* `taskTransport` is the per-turn transport when there is one. The harness
|
|
67
|
+
* object is passed through whole, because the RPC tools also read its
|
|
68
|
+
* `context` to fill `ContextInfo`; only the transport is swapped.
|
|
69
|
+
*/
|
|
70
|
+
export declare function channelsOf(gibson: Gibson, taskTransport?: ConnectTransport): RpcChannels;
|
|
71
|
+
/**
|
|
72
|
+
* The tools a posture carries: one per RPC, one per SDK helper, one per
|
|
73
|
+
* checked-in platform tool.
|
|
74
|
+
*
|
|
75
|
+
* A standalone posture holds no transport, so it carries no RPC tools: a
|
|
76
|
+
* tool with no daemon behind it answers every call with a dial error, which
|
|
77
|
+
* reads to a model like a broken platform rather than an unconnected
|
|
78
|
+
* session. Its helper tools are the ones that need no platform.
|
|
79
|
+
*
|
|
80
|
+
* Returns the discovery poller when this posture has a catalog to poll, so
|
|
81
|
+
* the caller can stop it on upgrade or close.
|
|
82
|
+
*/
|
|
83
|
+
export declare function registerPosture(group: ToolGroup, gibson: Gibson, ctx: PostureContext): Promise<Discovery | undefined>;
|