@zeroroot-ai/gibson-mcp 0.1.0 → 0.2.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/README.md CHANGED
@@ -36,22 +36,61 @@ snippets and their smoke tests live in
36
36
 
37
37
  ## The tool surface
38
38
 
39
- The surface is 1:1 with everything the SDK produces. One flat tier, no
40
- curated subset.
39
+ Coverage is 1:1 with everything the SDK produces. **Exposure** is in two
40
+ tiers, because 188 tool descriptions on every turn cost more context than
41
+ they are worth, bury the tools an agent reaches for, and trip the tool cap
42
+ some hosts impose.
43
+
44
+ ### Front tier, in `tools/list`
41
45
 
42
46
  | Where a tool comes from | Naming | How many |
43
47
  |---|---|---|
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
48
  | every SDK helper | the helper's own name: `remember`, `recall`, `world_view`, `submit_finding`, `delegate` | 30 |
46
49
  | every checked-in platform tool and plugin, discovered at runtime | `gibson_<tool>`, `gibson_plugin_<plugin>` | whatever the tenant has |
50
+ | the session's own tools | `gibson_status`, `gibson_login`, `gibson_connect`, `gibson_call_tool`, `ask` | 5 |
51
+ | the door to the full API | `gibson_api_search`, `gibson_api_call` | 2 |
52
+
53
+ Discovery repeats every 60 seconds and emits `tools/list_changed`, so a tool
54
+ a person enrols now is callable in the same session.
55
+
56
+ ### Full tier, behind the door
57
+
58
+ Every RPC of every SDK service, generated from the proto descriptors and
59
+ named `<service>_<method>` in snake case: **188 across 12 services** today.
60
+ They are all built and all callable. They are simply not listed.
61
+
62
+ ```
63
+ gibson_api_search("") -> the 12 services, with a count each
64
+ gibson_api_search("open a job on a bank") -> job_service_open_job, with its input schema
65
+ gibson_api_call("job_service_open_job", {...})
66
+ ```
67
+
68
+ `gibson_api_search` ranks on the RPC name, the service name and the proto
69
+ comment, and returns each match with its exact tool name, its description,
70
+ its service and the **full input JSON schema**, so one search is enough to
71
+ make the call. An empty query lists the services instead, so an agent can
72
+ orient before it searches. `gibson_api_call` runs the same handler the flat
73
+ tier would register, so the two paths cannot disagree; an unknown name comes
74
+ back with the three closest.
75
+
76
+ An SDK bump regenerates the full tier. A drift guard fails CI when the
77
+ generated table and the descriptors disagree, in either direction, so
78
+ hiding a tool never means losing one.
79
+
80
+ ### `--expose-all-rpcs`
47
81
 
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.
82
+ Registers the full tier in `tools/list` as well, which is what the server
83
+ did before this became two tiers. Off by default. A host with a large
84
+ context window and no tool cap can take the flat 1:1 surface. The door stays
85
+ open with the flag on, because finding one RPC among 188 is still cheaper
86
+ through a search than through the list.
51
87
 
52
88
  A posture registers only the tools its credential can reach. With no
53
89
  platform the server still serves `submit_finding`, `componentize` and
54
- `validate_component`, plus `gibson_login` and `gibson_connect`.
90
+ `validate_component`, plus `gibson_login` and `gibson_connect`, and no RPC
91
+ tool at all: a tool with no daemon behind it answers every call with a dial
92
+ error, which reads to a model like a broken platform rather than an
93
+ unconnected session.
55
94
 
56
95
  ## Resources and prompts
57
96
 
@@ -72,7 +111,8 @@ one calls the `gibson_ambient` prompt instead. Either way it is one lookup.
72
111
 
73
112
  `--listen` accepts a loopback address only, and DNS rebinding protection is
74
113
  on. `--stream-limit` (default 500) caps how many messages a server-streaming
75
- RPC tool returns before it reports `truncated`.
114
+ RPC tool returns before it reports `truncated`. `--expose-all-rpcs` lists the
115
+ full RPC tier as well; see the tool surface below.
76
116
 
77
117
  ### The HTTP routes
78
118
 
@@ -81,7 +121,7 @@ RPC tool returns before it reports `truncated`.
81
121
  | `POST /mcp` | starts an MCP session with `initialize`; later requests carry `mcp-session-id` |
82
122
  | `GET /mcp` | the session's notification stream |
83
123
  | `DELETE /mcp` | ends a session |
84
- | `GET /healthz` | liveness, plus the check-in source, the posture, the tool count and the open job |
124
+ | `GET /healthz` | liveness, plus the check-in source, the posture, the listed tool count, the reachable RPC count and the open job |
85
125
  | `POST /turn` | puts a dispatch's grant in force |
86
126
  | `GET /turn` | reports the turn in force |
87
127
  | `DELETE /turn` | ends it |
package/dist/api.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { ToolDefinition } from "./registry.js";
2
+ import type { RpcEntry } from "./rpc.js";
3
+ export declare const DEFAULT_SEARCH_LIMIT = 10;
4
+ export declare const MAX_SEARCH_LIMIT = 50;
5
+ export declare const SUGGESTIONS = 3;
6
+ export declare function terms(query: string): string[];
7
+ /**
8
+ * Rank one RPC against a query.
9
+ *
10
+ * The weights say what a person searching for an RPC is actually naming: the
11
+ * method first, the service second, the prose last. A whole-word hit beats a
12
+ * substring, so "job" does not rank `job_service_get_job` below something
13
+ * that merely contains "jobs" in a sentence.
14
+ */
15
+ export declare function score(entry: RpcEntry, query: string): number;
16
+ export interface SearchHit {
17
+ name: string;
18
+ description: string;
19
+ service: string;
20
+ input_schema: unknown;
21
+ score: number;
22
+ }
23
+ /** Rank the catalog. Ties break on the shorter name, then alphabetically. */
24
+ export declare function search(catalog: RpcEntry[], query: string, limit: number): SearchHit[];
25
+ /** The services in the catalog, with a method count each. */
26
+ export declare function services(catalog: RpcEntry[]): {
27
+ service: string;
28
+ tools: number;
29
+ example: string;
30
+ }[];
31
+ /** Dice coefficient over character bigrams. Survives a typo; an equality test does not. */
32
+ export declare function similarity(a: string, b: string): number;
33
+ /** The names closest to `name`, for a caller that mistyped one. */
34
+ export declare function closest(catalog: RpcEntry[], name: string, count?: number): string[];
35
+ export interface ApiToolOptions {
36
+ catalog: RpcEntry[];
37
+ /** True when the flat set is also in `tools/list`, which changes what to say. */
38
+ exposeAll?: boolean;
39
+ }
40
+ export declare function apiTools(opts: ApiToolOptions): ToolDefinition[];
package/dist/api.js ADDED
@@ -0,0 +1,216 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./tool.js";
3
+ import { failure, json } from "./tools/result.js";
4
+ /**
5
+ * The door to the full API (sdk-ts#70).
6
+ *
7
+ * The platform's whole surface is one tool per RPC, and there are 188 of
8
+ * them. Every host loads every description into the model context on each
9
+ * turn, so putting all of them in `tools/list` costs the same tokens whether
10
+ * or not the agent needs a single one, buries the tools it actually reaches
11
+ * for, and trips the tool cap some hosts impose.
12
+ *
13
+ * So the generated tools stay built and stay 1:1 with the descriptors, and
14
+ * two tools stand in front of them: one to find an RPC, one to call it.
15
+ * Coverage is unchanged. Only exposure changed, and `--expose-all-rpcs`
16
+ * puts the flat set back in `tools/list` for a host that wants it.
17
+ */
18
+ /** Words that carry no signal in a search over RPC names. */
19
+ const STOPWORDS = new Set(["a", "an", "the", "to", "for", "of", "in", "on", "with", "and", "or", "is", "are", "be", "please", "me", "my"]);
20
+ export const DEFAULT_SEARCH_LIMIT = 10;
21
+ export const MAX_SEARCH_LIMIT = 50;
22
+ export const SUGGESTIONS = 3;
23
+ export function terms(query) {
24
+ return query
25
+ .toLowerCase()
26
+ .split(/[^a-z0-9]+/)
27
+ .filter((t) => t.length > 1 && !STOPWORDS.has(t));
28
+ }
29
+ /** True when `haystack` holds `term` as a whole underscore- or space-separated word. */
30
+ function wholeWord(haystack, term) {
31
+ return new RegExp(`(^|[^a-z0-9])${term}([^a-z0-9]|$)`).test(haystack);
32
+ }
33
+ /**
34
+ * Rank one RPC against a query.
35
+ *
36
+ * The weights say what a person searching for an RPC is actually naming: the
37
+ * method first, the service second, the prose last. A whole-word hit beats a
38
+ * substring, so "job" does not rank `job_service_get_job` below something
39
+ * that merely contains "jobs" in a sentence.
40
+ */
41
+ export function score(entry, query) {
42
+ const words = terms(query);
43
+ if (words.length === 0)
44
+ return 0;
45
+ const method = entry.method.name.toLowerCase();
46
+ const methodSnake = entry.tool.name.slice(entry.tool.name.length - method.length);
47
+ const service = entry.service.name.toLowerCase();
48
+ const comment = entry.comment.toLowerCase();
49
+ const toolName = entry.tool.name;
50
+ let total = 0;
51
+ let hit = 0;
52
+ for (const term of words) {
53
+ let best = 0;
54
+ if (wholeWord(methodSnake, term))
55
+ best = 10;
56
+ else if (methodSnake.includes(term))
57
+ best = 6;
58
+ if (best === 0 && wholeWord(service, term))
59
+ best = 4;
60
+ else if (best === 0 && service.includes(term))
61
+ best = 2;
62
+ if (best === 0 && wholeWord(comment, term))
63
+ best = 3;
64
+ else if (best === 0 && comment.includes(term))
65
+ best = 1;
66
+ if (best > 0)
67
+ hit += 1;
68
+ total += best;
69
+ }
70
+ if (total === 0)
71
+ return 0;
72
+ // A query whose every word landed beats one that matched half of itself,
73
+ // whatever the raw weights add up to.
74
+ total += hit === words.length ? 8 : 0;
75
+ // The whole phrase written as a tool name is the strongest signal there is.
76
+ if (toolName.includes(words.join("_")))
77
+ total += 8;
78
+ return total;
79
+ }
80
+ /** Rank the catalog. Ties break on the shorter name, then alphabetically. */
81
+ export function search(catalog, query, limit) {
82
+ return catalog
83
+ .map((entry) => ({ entry, score: score(entry, query) }))
84
+ .filter((s) => s.score > 0)
85
+ .sort((a, b) => b.score - a.score || a.entry.tool.name.length - b.entry.tool.name.length || (a.entry.tool.name < b.entry.tool.name ? -1 : 1))
86
+ .slice(0, limit)
87
+ .map(({ entry, score: value }) => ({
88
+ name: entry.tool.name,
89
+ description: entry.tool.description,
90
+ service: entry.service.typeName,
91
+ input_schema: entry.tool.inputSchema,
92
+ score: value,
93
+ }));
94
+ }
95
+ /** The services in the catalog, with a method count each. */
96
+ export function services(catalog) {
97
+ const byService = new Map();
98
+ for (const entry of catalog) {
99
+ const list = byService.get(entry.service.typeName) ?? [];
100
+ list.push(entry);
101
+ byService.set(entry.service.typeName, list);
102
+ }
103
+ return [...byService.entries()]
104
+ .sort(([a], [b]) => (a < b ? -1 : 1))
105
+ .map(([name, entries]) => ({ service: name, tools: entries.length, example: entries[0].tool.name }));
106
+ }
107
+ /** Dice coefficient over character bigrams. Survives a typo; an equality test does not. */
108
+ export function similarity(a, b) {
109
+ const grams = (s) => Array.from({ length: Math.max(0, s.length - 1) }, (_, i) => s.slice(i, i + 2));
110
+ const left = grams(a);
111
+ const right = new Map();
112
+ for (const g of grams(b))
113
+ right.set(g, (right.get(g) ?? 0) + 1);
114
+ let shared = 0;
115
+ for (const g of left) {
116
+ const n = right.get(g) ?? 0;
117
+ if (n > 0) {
118
+ shared += 1;
119
+ right.set(g, n - 1);
120
+ }
121
+ }
122
+ return left.length + grams(b).length === 0 ? 0 : (2 * shared) / (left.length + grams(b).length);
123
+ }
124
+ /** The names closest to `name`, for a caller that mistyped one. */
125
+ export function closest(catalog, name, count = SUGGESTIONS) {
126
+ return catalog
127
+ .map((entry) => ({ name: entry.tool.name, score: similarity(name.toLowerCase(), entry.tool.name) }))
128
+ .sort((a, b) => b.score - a.score || (a.name < b.name ? -1 : 1))
129
+ .slice(0, count)
130
+ .map((c) => c.name);
131
+ }
132
+ /**
133
+ * The description both meta-tools carry.
134
+ *
135
+ * It has one job: tell a model the door exists. A model that never reads the
136
+ * 188 entries has no other way to learn that `CreateBank`, `OpenJob` or
137
+ * `PutSessionContext` are reachable at all, so the examples are not
138
+ * decoration.
139
+ */
140
+ function surfaceLine(catalog) {
141
+ const count = catalog.length;
142
+ const serviceCount = new Set(catalog.map((e) => e.service.typeName)).size;
143
+ return (`The Gibson platform's whole API is reachable this way: ${count} RPCs across ${serviceCount} services, ` +
144
+ "including banks of always-on agents (CreateBank, ListMembers), jobs (OpenJob, SendInput, CloseJob), " +
145
+ "missions (CreateMission, RunMission), the knowledge graph (QueryNodes, Observe), findings, targets, " +
146
+ "component registration, credentials and the session store. These RPCs are not in your tool list, " +
147
+ "because listing all of them would cost more context than it is worth. Search here first.");
148
+ }
149
+ export function apiTools(opts) {
150
+ const { catalog } = opts;
151
+ const byName = new Map(catalog.map((e) => [e.tool.name, e]));
152
+ const surface = surfaceLine(catalog);
153
+ return [
154
+ defineTool({
155
+ name: "gibson_api_search",
156
+ description: `Find an RPC on the Gibson platform API. ${surface} ` +
157
+ "Returns each match with its exact tool name, its description, its service, and the full JSON " +
158
+ "schema of its input, so the result is everything you need to call it with gibson_api_call. " +
159
+ 'Call it with an empty query to see the services and how many RPCs each one has, e.g. query "" first, ' +
160
+ 'then query "open a job" or "submit a finding".',
161
+ input: {
162
+ query: z
163
+ .string()
164
+ .describe('What you want to do, in your own words, e.g. "submit a finding" or "list the members of a bank". Empty lists the services instead.'),
165
+ limit: z.number().int().min(1).max(MAX_SEARCH_LIMIT).optional().describe(`Maximum matches. Defaults to ${DEFAULT_SEARCH_LIMIT}, capped at ${MAX_SEARCH_LIMIT}.`),
166
+ },
167
+ annotations: { readOnlyHint: true },
168
+ handler: async (args) => {
169
+ if (catalog.length === 0) {
170
+ return failure("no platform API", "This session is not checked in to a platform, so no RPC is reachable. Call gibson_status.");
171
+ }
172
+ // An empty query is an agent orienting itself, not a failed search.
173
+ // Answering with "no matches" would teach it the API is empty.
174
+ if (!args.query.trim()) {
175
+ return json({ services: services(catalog), rpcs: catalog.length, next: 'Search again with what you want to do, e.g. "open a job".' });
176
+ }
177
+ const limit = Math.min(args.limit ?? DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT);
178
+ const hits = search(catalog, args.query, limit);
179
+ if (hits.length === 0) {
180
+ return json({
181
+ query: args.query,
182
+ matches: [],
183
+ services: services(catalog).map((s) => s.service),
184
+ next: "Nothing matched. Try one word from the thing you want to act on, or search with an empty query to see the services.",
185
+ });
186
+ }
187
+ return json({ query: args.query, matches: hits, showing: hits.length, of: catalog.length });
188
+ },
189
+ }),
190
+ defineTool({
191
+ name: "gibson_api_call",
192
+ description: `Call one RPC on the Gibson platform API by its exact tool name. ${surface} ` +
193
+ "Use gibson_api_search first to get the name and the input schema. The input must be canonical " +
194
+ "protojson for that RPC's request message. A name that does not exist comes back with the closest " +
195
+ "ones, so a near miss costs one call rather than a guess.",
196
+ input: {
197
+ name: z.string().describe('Exact tool name from gibson_api_search, e.g. "harness_callback_service_world_view".'),
198
+ input: z.record(z.string(), z.unknown()).optional().describe("The request message as protojson. Omit for an RPC whose request has no fields."),
199
+ timeout_ms: z.number().int().min(1).optional().describe("Per-call deadline in milliseconds."),
200
+ },
201
+ handler: async (args, ctx) => {
202
+ if (catalog.length === 0) {
203
+ return failure("no platform API", "This session is not checked in to a platform, so no RPC is reachable. Call gibson_status.");
204
+ }
205
+ const entry = byName.get(args.name);
206
+ if (!entry) {
207
+ const suggestions = closest(catalog, args.name);
208
+ return failure(`no RPC named ${args.name}`, `The closest names are:\n${suggestions.map((s) => `- ${s}`).join("\n")}\n\nCall gibson_api_search to find the right one.`);
209
+ }
210
+ // The same handler the flat tier registers, so the two paths cannot
211
+ // drift: there is only one of them.
212
+ return entry.tool.handler(args.input ?? {}, { ...ctx, ...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}) });
213
+ },
214
+ }),
215
+ ];
216
+ }
package/dist/build.d.ts CHANGED
@@ -5,7 +5,7 @@ import { ToolRegistry, type ToolDefinition, type ToolGroup } from "./registry.js
5
5
  import type { TurnRoute } from "./http.js";
6
6
  import { type Discovery } from "./discovery.js";
7
7
  import { type Inbox } from "./inbox.js";
8
- import { type RpcChannels } from "./rpc.js";
8
+ import { type RpcChannels, type RpcEntry } from "./rpc.js";
9
9
  import { type AmbientSource } from "./resources.js";
10
10
  import { type Gibson } from "./session.js";
11
11
  import { type ConnectDeps } from "./tools/connect.js";
@@ -23,6 +23,8 @@ import { type ConnectDeps } from "./tools/connect.js";
23
23
  */
24
24
  export interface BuildDeps extends ConnectDeps {
25
25
  streamLimit?: number;
26
+ /** Register the generated RPC tools in `tools/list` as well. Default off. */
27
+ exposeAllRpcs?: boolean;
26
28
  /** How often to look for newly checked-in platform tools. `0` runs one pass. */
27
29
  discoveryIntervalMs?: number;
28
30
  }
@@ -52,6 +54,8 @@ export interface PostureContext {
52
54
  env: NodeJS.ProcessEnv;
53
55
  cwd: string;
54
56
  streamLimit: number;
57
+ /** Register the generated RPC tools in `tools/list` as well. Default off. */
58
+ exposeAllRpcs?: boolean;
55
59
  /** The session's ambient block, for the hook handoff. */
56
60
  ambient?: AmbientSource;
57
61
  discoveryIntervalMs?: number;
@@ -59,6 +63,8 @@ export interface PostureContext {
59
63
  taskTransport?: ConnectTransport;
60
64
  /** The `ask` tool, when this posture has an inbox to ask through. */
61
65
  ask?: ToolDefinition;
66
+ /** Told the full tier this posture built, for /healthz. */
67
+ onCatalog?: (catalog: RpcEntry[]) => void;
62
68
  }
63
69
  /**
64
70
  * Which transports this posture holds, for the generated RPC tools.
package/dist/build.js CHANGED
@@ -7,7 +7,8 @@ import { AnswerRouter, askTool } from "./ask.js";
7
7
  import { startDiscovery } from "./discovery.js";
8
8
  import { inboxAvailable, openInbox, routeAnswers } from "./inbox.js";
9
9
  import { helperToolsFor } from "./helpers/index.js";
10
- import { rpcTools } from "./rpc.js";
10
+ import { apiTools } from "./api.js";
11
+ import { rpcCatalog } from "./rpc.js";
11
12
  import { createTurnController, TURN_GRANT_HEADER } from "./turn.js";
12
13
  import { ambientPrompt, ambientSource, resources } from "./resources.js";
13
14
  import { openGibson } from "./session.js";
@@ -28,6 +29,10 @@ export async function buildSurface(env, cwd, deps = {}) {
28
29
  let inbox;
29
30
  const answers = new AnswerRouter();
30
31
  let ambient = ambientSource(gibson, env);
32
+ // The full tier is built but usually not listed, so its size is not the
33
+ // registry's size. /healthz reports both, because a driver checking the
34
+ // server is up also wants to know the API is reachable.
35
+ let fullTier = 0;
31
36
  if (gibson.live) {
32
37
  turns = createTurnController({ base: gibson.live.harness, insecure: gibson.settings.callbackInsecure, log });
33
38
  registry.use((ctx, next) => {
@@ -49,9 +54,13 @@ export async function buildSurface(env, cwd, deps = {}) {
49
54
  env,
50
55
  cwd,
51
56
  streamLimit,
57
+ ...(deps.exposeAllRpcs ? { exposeAllRpcs: true } : {}),
52
58
  ...(deps.discoveryIntervalMs === undefined ? {} : { discoveryIntervalMs: deps.discoveryIntervalMs }),
53
59
  ...(turns ? { taskTransport: turns.transport() } : {}),
54
60
  ambient: { block: (q) => ambient.block(q) },
61
+ onCatalog: (catalog) => {
62
+ fullTier = catalog.length;
63
+ },
55
64
  ...(inbox
56
65
  ? {
57
66
  ask: askTool({ jobId: () => turns?.current()?.jobId, inbox, answers, log }),
@@ -80,7 +89,13 @@ export async function buildSurface(env, cwd, deps = {}) {
80
89
  resources: resources(() => gibson, { block: (q) => ambient.block(q) }),
81
90
  prompts: [ambientPrompt({ block: (q) => ambient.block(q) })],
82
91
  }, transport),
83
- health: () => ({ source: gibson.source, posture: gibson.mode, tools: registry.size(), ...(turns?.current() ? { job: turns.current().jobId } : {}) }),
92
+ health: () => ({
93
+ source: gibson.source,
94
+ posture: gibson.mode,
95
+ tools: registry.size(),
96
+ api_rpcs: fullTier,
97
+ ...(turns?.current() ? { job: turns.current().jobId } : {}),
98
+ }),
84
99
  ...(inbox ? { inbox } : {}),
85
100
  ...(turns
86
101
  ? {
@@ -148,11 +163,27 @@ export async function registerPosture(group, gibson, ctx) {
148
163
  group.register(tool);
149
164
  if (ctx.ask)
150
165
  group.register(ctx.ask);
166
+ // Two tiers (sdk-ts#70). The generated tools are always BUILT, and the
167
+ // drift guard proves that set is 1:1 with the descriptors. What changes
168
+ // here is only whether they are listed: by default they sit behind
169
+ // gibson_api_search and gibson_api_call, because 188 descriptions on every
170
+ // turn bury the tools an agent reaches for and trip some hosts' tool caps.
151
171
  const channels = channelsOf(gibson, ctx.taskTransport);
172
+ let catalog = [];
152
173
  if (channels.session || channels.task) {
153
- for (const tool of rpcTools({ channels, streamLimit: ctx.streamLimit }))
174
+ catalog = rpcCatalog({ channels, streamLimit: ctx.streamLimit });
175
+ if (ctx.exposeAllRpcs) {
176
+ for (const entry of catalog)
177
+ group.register(entry.tool);
178
+ }
179
+ // The door is registered either way. With the flat set in front it is
180
+ // still the cheapest way to find one RPC among 188.
181
+ for (const tool of apiTools({ catalog, ...(ctx.exposeAllRpcs ? { exposeAll: true } : {}) }))
154
182
  group.register(tool);
183
+ log(`${TAG} ${catalog.length} RPC tool(s) ${ctx.exposeAllRpcs ? "listed and reachable" : "reachable"} through gibson_api_search and gibson_api_call` +
184
+ `${ctx.exposeAllRpcs ? " (--expose-all-rpcs)" : ""}`);
155
185
  }
186
+ ctx.onCatalog?.(catalog);
156
187
  await writeHandoff(gibson, ctx);
157
188
  // Discovery reads the tenant catalog, which is a ComponentService call, so
158
189
  // it needs the component check-in. A dispatched run has none.
@@ -205,5 +236,7 @@ async function writeHandoff(gibson, ctx) {
205
236
  }
206
237
  function instructions() {
207
238
  return ("Gibson tools. Call gibson_status to see how this session is connected. " +
208
- "Without a platform, call gibson_login and then gibson_connect to enroll this host and start a live mission.");
239
+ "Without a platform, call gibson_login and then gibson_connect to enroll this host and start a live mission. " +
240
+ "The tools listed here are the ones used most; the platform's whole API is reachable through " +
241
+ "gibson_api_search and gibson_api_call.");
209
242
  }
package/dist/flags.d.ts CHANGED
@@ -10,6 +10,11 @@
10
10
  * network interface.
11
11
  * - `--stream-limit <n>`: how many messages a server-streaming RPC tool
12
12
  * collects before it returns with `truncated: true` (slice A2).
13
+ * - `--expose-all-rpcs`: put the generated RPC tools in `tools/list` as
14
+ * well, which is what the server did before sdk-ts#70. Off by default:
15
+ * 188 descriptions on every turn bury the tools an agent reaches for, and
16
+ * some hosts cap the tool count. They are always reachable through
17
+ * `gibson_api_search` and `gibson_api_call`, flag or no flag.
13
18
  */
14
19
  export type TransportKind = "stdio" | "http";
15
20
  export interface Listen {
@@ -20,6 +25,8 @@ export interface Flags {
20
25
  transport: TransportKind;
21
26
  listen: Listen;
22
27
  streamLimit: number;
28
+ /** Register the generated RPC tools in `tools/list` as well. */
29
+ exposeAllRpcs: boolean;
23
30
  help: boolean;
24
31
  version: boolean;
25
32
  }
package/dist/flags.js CHANGED
@@ -43,6 +43,7 @@ export function parseFlags(argv) {
43
43
  transport: { type: "string", default: "stdio" },
44
44
  listen: { type: "string", default: DEFAULT_LISTEN },
45
45
  "stream-limit": { type: "string", default: String(DEFAULT_STREAM_LIMIT) },
46
+ "expose-all-rpcs": { type: "boolean", default: false },
46
47
  help: { type: "boolean", short: "h", default: false },
47
48
  version: { type: "boolean", default: false },
48
49
  },
@@ -59,6 +60,7 @@ export function parseFlags(argv) {
59
60
  transport,
60
61
  listen: parseListen(values.listen),
61
62
  streamLimit,
63
+ exposeAllRpcs: values["expose-all-rpcs"],
62
64
  help: values.help,
63
65
  version: values.version,
64
66
  };
@@ -74,6 +76,8 @@ export function usage() {
74
76
  " --transport stdio|http stdio (default) for a host that spawns the server; http for a sandbox.",
75
77
  ` --listen host:port loopback address for the HTTP transport (default ${DEFAULT_LISTEN}).`,
76
78
  ` --stream-limit n messages a streaming RPC tool returns before it truncates (default ${DEFAULT_STREAM_LIMIT}).`,
79
+ " --expose-all-rpcs also list every generated RPC tool in tools/list. Off by default: they are",
80
+ " reachable through gibson_api_search and gibson_api_call either way.",
77
81
  " --version print the package version.",
78
82
  " -h, --help this text.",
79
83
  "",
package/dist/index.d.ts CHANGED
@@ -8,7 +8,8 @@ export { attachServer, packageVersion, SERVER_NAME } from "./server.js";
8
8
  export { ToolRegistry, type JsonSchema, type ToolContext, type ToolDefinition, type ToolGroup, type ToolHandler } from "./registry.js";
9
9
  export { defineTool, jsonSchemaOf, type ToolSpec } from "./tool.js";
10
10
  export { openGibson, type Gibson, type OpenGibsonOptions } from "./session.js";
11
- export { rpcTools, snake, toolNameFor, transportFor, type RpcChannels, type RpcToolOptions } from "./rpc.js";
11
+ export { rpcCatalog, rpcTools, driftBetween, snake, toolNameFor, transportFor, type Drift, type RpcChannels, type RpcEntry, type RpcToolOptions, type ServiceDoc } from "./rpc.js";
12
+ export { apiTools, closest, score, search, services, similarity, terms, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT, type ApiToolOptions, type SearchHit, } from "./api.js";
12
13
  export { messageSchema, requestSchema, MAX_DEPTH } from "./schema.js";
13
14
  export { startDiscovery, toolKey, pluginKey, DISCOVERY_INTERVAL_MS, type Discovery, type DiscoveryOutcome } from "./discovery.js";
14
15
  export { helperTools, helperToolsFor, helperContext, HELPER_TOOL_FOR_EXPORT, NOT_A_TOOL, TOOL_WITHOUT_EXPORT, type HelperContext } from "./helpers/index.js";
package/dist/index.js CHANGED
@@ -10,7 +10,8 @@ export { attachServer, packageVersion, SERVER_NAME } from "./server.js";
10
10
  export { ToolRegistry } from "./registry.js";
11
11
  export { defineTool, jsonSchemaOf } from "./tool.js";
12
12
  export { openGibson } from "./session.js";
13
- export { rpcTools, snake, toolNameFor, transportFor } from "./rpc.js";
13
+ export { rpcCatalog, rpcTools, driftBetween, snake, toolNameFor, transportFor } from "./rpc.js";
14
+ export { apiTools, closest, score, search, services, similarity, terms, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT, } from "./api.js";
14
15
  export { messageSchema, requestSchema, MAX_DEPTH } from "./schema.js";
15
16
  export { startDiscovery, toolKey, pluginKey, DISCOVERY_INTERVAL_MS } from "./discovery.js";
16
17
  export { helperTools, helperToolsFor, helperContext, HELPER_TOOL_FOR_EXPORT, NOT_A_TOOL, TOOL_WITHOUT_EXPORT } from "./helpers/index.js";
package/dist/main.js CHANGED
@@ -30,7 +30,7 @@ async function main() {
30
30
  return;
31
31
  }
32
32
  const cwd = process.cwd();
33
- const surface = await buildSurface(process.env, cwd, { streamLimit: flags.streamLimit });
33
+ const surface = await buildSurface(process.env, cwd, { streamLimit: flags.streamLimit, ...(flags.exposeAllRpcs ? { exposeAllRpcs: true } : {}) });
34
34
  log(`${TAG} ${describeGibson(surface.current()).replaceAll("\n", "; ")}`);
35
35
  log(`${TAG} ${surface.registry.size()} tool(s) registered`);
36
36
  let closing = false;
@@ -9,6 +9,8 @@ export interface ToolContext {
9
9
  /** Request headers of the transport, when it has any (the HTTP transport). */
10
10
  headers?: Record<string, string | string[] | undefined>;
11
11
  signal?: AbortSignal;
12
+ /** Per-call deadline, when the caller set one. `gibson_api_call` does. */
13
+ timeoutMs?: number;
12
14
  }
13
15
  export type ToolHandler = (args: Record<string, unknown>, ctx: ToolContext) => Promise<CallToolResult>;
14
16
  /**
package/dist/rpc.d.ts CHANGED
@@ -3,14 +3,21 @@ import { type Transport } from "@connectrpc/connect";
3
3
  import type { TaskHarness } from "@zeroroot-ai/sdk";
4
4
  import type { ToolDefinition } from "./registry.js";
5
5
  /**
6
- * One MCP tool per RPC of every service the SDK produces (gibson#1706,
7
- * decision 2). No curated subset and no hand-written list: the table comes
8
- * from the descriptors, so an SDK bump moves the tool set.
6
+ * One tool per RPC of every service the SDK produces (gibson#1706, decision
7
+ * 2). No curated subset and no hand-written list: the table comes from the
8
+ * descriptors, so an SDK bump moves the tool set.
9
9
  *
10
10
  * Naming is `<service>_<method>` in snake case, e.g.
11
11
  * `harness_callback_service_world_view`. The service prefix is what keeps
12
12
  * these apart from the helper tools, which are named after the helper
13
13
  * (`world_view`, `remember`, `submit_finding`).
14
+ *
15
+ * COVERAGE IS NOT EXPOSURE. Every one of these is built, and the drift guard
16
+ * proves the set is 1:1 with the descriptors. Whether they appear in
17
+ * `tools/list` is a separate decision, taken in build.ts: by default they sit
18
+ * behind `gibson_api_search` and `gibson_api_call` (sdk-ts#70), because 188
19
+ * descriptions on every turn bury the tools an agent reaches for.
20
+ * `--expose-all-rpcs` puts them in front as well.
14
21
  */
15
22
  /** Where an RPC is reached. */
16
23
  export interface RpcChannels {
@@ -46,6 +53,14 @@ export interface RpcToolOptions {
46
53
  /** Messages a server-streaming RPC returns before it truncates. */
47
54
  streamLimit: number;
48
55
  }
56
+ /** One built RPC tool, with the descriptor facts a search can rank on. */
57
+ export interface RpcEntry {
58
+ tool: ToolDefinition;
59
+ service: DescService;
60
+ method: DescMethod;
61
+ /** The RPC's proto comment, or "" when it carries none. */
62
+ comment: string;
63
+ }
49
64
  /**
50
65
  * Build every RPC tool that has a transport to ride.
51
66
  *
@@ -53,6 +68,8 @@ export interface RpcToolOptions {
53
68
  * no daemon behind it would answer every call with a dial error, which reads
54
69
  * to a model like a broken platform rather than an unconnected session.
55
70
  */
71
+ export declare function rpcCatalog(opts: RpcToolOptions): RpcEntry[];
72
+ /** The same set as {@link rpcCatalog}, as bare tool definitions. */
56
73
  export declare function rpcTools(opts: RpcToolOptions): ToolDefinition[];
57
74
  /** An empty request message, for a test that needs one. */
58
75
  export declare function emptyRequest(method: DescMethod): unknown;
package/dist/rpc.js CHANGED
@@ -78,7 +78,7 @@ async function collect(stream, output, limit) {
78
78
  * no daemon behind it would answer every call with a dial error, which reads
79
79
  * to a model like a broken platform rather than an unconnected session.
80
80
  */
81
- export function rpcTools(opts) {
81
+ export function rpcCatalog(opts) {
82
82
  const out = [];
83
83
  for (const entry of GENERATED_SERVICES) {
84
84
  const service = entry.service;
@@ -91,11 +91,16 @@ export function rpcTools(opts) {
91
91
  // becomes a tool, and the drift test is what says the table is stale.
92
92
  const client = createClient(service, transport);
93
93
  for (const method of service.methods) {
94
- out.push(rpcTool(service, method, client, comments.get(method.localName) ?? "", opts));
94
+ const comment = comments.get(method.localName) ?? "";
95
+ out.push({ tool: rpcTool(service, method, client, comment, opts), service, method, comment });
95
96
  }
96
97
  }
97
98
  return out;
98
99
  }
100
+ /** The same set as {@link rpcCatalog}, as bare tool definitions. */
101
+ export function rpcTools(opts) {
102
+ return rpcCatalog(opts).map((e) => e.tool);
103
+ }
99
104
  function rpcTool(service, method, client, comment, opts) {
100
105
  const contextKey = contextField(method.input);
101
106
  const streaming = method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming";
@@ -134,7 +139,7 @@ async function decode(service, method, client, contextKey, streaming, args, ctx,
134
139
  const call = client[method.localName];
135
140
  if (!call)
136
141
  return failure(`${method.name} is unavailable`, `The generated client for ${service.typeName} has no ${method.localName}.`);
137
- const options = { signal: ctx.signal };
142
+ const options = { signal: ctx.signal, ...(ctx.timeoutMs ? { timeoutMs: ctx.timeoutMs } : {}) };
138
143
  if (method.methodKind === "unary") {
139
144
  const res = await call(messages[0], options);
140
145
  return json(toJson(method.output, res));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroroot-ai/gibson-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The Gibson MCP server: one tool surface for every coding agent host. Every RPC of every SDK service, every SDK helper and every checked-in platform tool, over stdio or streamable HTTP on localhost.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,19 +19,13 @@
19
19
  "files": [
20
20
  "dist"
21
21
  ],
22
- "scripts": {
23
- "generate": "node scripts/generate-tools.ts",
24
- "build": "tsc -p tsconfig.json",
25
- "typecheck": "tsc -p tsconfig.json --noEmit",
26
- "test": "sh -c 'tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"' swallow-forwarded-vitest-args"
27
- },
28
22
  "dependencies": {
29
23
  "@bufbuild/protobuf": "^2.2.0",
30
24
  "@connectrpc/connect": "^2.0.0",
31
25
  "@connectrpc/connect-node": "^2.0.0",
32
26
  "@modelcontextprotocol/sdk": "^1.30.0",
33
- "@zeroroot-ai/sdk": "workspace:^",
34
- "zod": "^4.4.3"
27
+ "zod": "^4.4.3",
28
+ "@zeroroot-ai/sdk": "^0.12.0"
35
29
  },
36
30
  "devDependencies": {
37
31
  "@types/node": "^22.0.0",
@@ -54,5 +48,11 @@
54
48
  "opencode",
55
49
  "security",
56
50
  "knowledge-graph"
57
- ]
58
- }
51
+ ],
52
+ "scripts": {
53
+ "generate": "node scripts/generate-tools.ts",
54
+ "build": "tsc -p tsconfig.json",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "test": "sh -c 'tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"' swallow-forwarded-vitest-args"
57
+ }
58
+ }