@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Gibson } from "./session.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ambient knowledge and session coordinates, as MCP resources.
|
|
4
|
+
*
|
|
5
|
+
* A host with a hook surface injects the ambient block itself: Claude Code
|
|
6
|
+
* reads it at SessionStart, the opencode plugin at `system.transform`. A
|
|
7
|
+
* host with no hook surface has nowhere to put it, so the same block is also
|
|
8
|
+
* an MCP resource the model can read and a prompt it can call. One lookup
|
|
9
|
+
* either way: the block is computed once per connection and cached, because
|
|
10
|
+
* it is prompt overhead on every session.
|
|
11
|
+
*
|
|
12
|
+
* `gibson://session` carries the live-mission coordinates a session-end hook
|
|
13
|
+
* needs to checkpoint the transcript (`PutSessionContext`). A hook runs as
|
|
14
|
+
* its own process and cannot reach this server, so it reads the same
|
|
15
|
+
* coordinates from the state file; the resource is for a host that can ask
|
|
16
|
+
* the server directly.
|
|
17
|
+
*/
|
|
18
|
+
export declare const AMBIENT_URI = "gibson://ambient";
|
|
19
|
+
export declare const SESSION_URI = "gibson://session";
|
|
20
|
+
export declare const DEFAULT_AMBIENT_QUERY = "memories, prior findings and security facts for this codebase";
|
|
21
|
+
export interface ResourceContents {
|
|
22
|
+
uri: string;
|
|
23
|
+
mimeType: string;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ResourceDefinition {
|
|
27
|
+
uri: string;
|
|
28
|
+
name: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
read(): Promise<ResourceContents>;
|
|
33
|
+
}
|
|
34
|
+
export interface PromptDefinition {
|
|
35
|
+
name: string;
|
|
36
|
+
title: string;
|
|
37
|
+
description: string;
|
|
38
|
+
arguments: {
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
required: boolean;
|
|
42
|
+
}[];
|
|
43
|
+
get(args: Record<string, string>): Promise<{
|
|
44
|
+
description: string;
|
|
45
|
+
messages: {
|
|
46
|
+
role: "user";
|
|
47
|
+
content: {
|
|
48
|
+
type: "text";
|
|
49
|
+
text: string;
|
|
50
|
+
};
|
|
51
|
+
}[];
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
/** The session's ambient block, computed once and kept. */
|
|
55
|
+
export interface AmbientSource {
|
|
56
|
+
block(query?: string): Promise<string>;
|
|
57
|
+
}
|
|
58
|
+
export declare function ambientSource(gibson: Gibson, env: NodeJS.ProcessEnv): AmbientSource;
|
|
59
|
+
/** What a session-end hook needs to checkpoint a transcript. */
|
|
60
|
+
export declare function sessionCoordinates(gibson: Gibson): Record<string, unknown>;
|
|
61
|
+
export declare function resources(gibson: () => Gibson, ambient: AmbientSource): ResourceDefinition[];
|
|
62
|
+
/** The ambient block as a prompt, for a host with no hook surface. */
|
|
63
|
+
export declare function ambientPrompt(ambient: AmbientSource): PromptDefinition;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ambientBlock } from "./ambient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Ambient knowledge and session coordinates, as MCP resources.
|
|
5
|
+
*
|
|
6
|
+
* A host with a hook surface injects the ambient block itself: Claude Code
|
|
7
|
+
* reads it at SessionStart, the opencode plugin at `system.transform`. A
|
|
8
|
+
* host with no hook surface has nowhere to put it, so the same block is also
|
|
9
|
+
* an MCP resource the model can read and a prompt it can call. One lookup
|
|
10
|
+
* either way: the block is computed once per connection and cached, because
|
|
11
|
+
* it is prompt overhead on every session.
|
|
12
|
+
*
|
|
13
|
+
* `gibson://session` carries the live-mission coordinates a session-end hook
|
|
14
|
+
* needs to checkpoint the transcript (`PutSessionContext`). A hook runs as
|
|
15
|
+
* its own process and cannot reach this server, so it reads the same
|
|
16
|
+
* coordinates from the state file; the resource is for a host that can ask
|
|
17
|
+
* the server directly.
|
|
18
|
+
*/
|
|
19
|
+
export const AMBIENT_URI = "gibson://ambient";
|
|
20
|
+
export const SESSION_URI = "gibson://session";
|
|
21
|
+
export const DEFAULT_AMBIENT_QUERY = "memories, prior findings and security facts for this codebase";
|
|
22
|
+
export function ambientSource(gibson, env) {
|
|
23
|
+
const defaultQuery = env.ZEROCOOL_AMBIENT_QUERY ?? DEFAULT_AMBIENT_QUERY;
|
|
24
|
+
let cached;
|
|
25
|
+
return {
|
|
26
|
+
block: (query) => {
|
|
27
|
+
const knowledge = gibson.knowledge;
|
|
28
|
+
if (!knowledge)
|
|
29
|
+
return Promise.resolve("");
|
|
30
|
+
// Only the default query is cached. A model that asks its own question
|
|
31
|
+
// wants an answer to that question, not the session's opening block.
|
|
32
|
+
if (query && query !== defaultQuery)
|
|
33
|
+
return ambientBlock(knowledge, query);
|
|
34
|
+
cached ??= ambientBlock(knowledge, defaultQuery);
|
|
35
|
+
return cached;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** What a session-end hook needs to checkpoint a transcript. */
|
|
40
|
+
export function sessionCoordinates(gibson) {
|
|
41
|
+
return {
|
|
42
|
+
source: gibson.source,
|
|
43
|
+
posture: gibson.mode,
|
|
44
|
+
agent_name: gibson.agentName,
|
|
45
|
+
...(gibson.settings.platformURL ? { platform_url: gibson.settings.platformURL } : {}),
|
|
46
|
+
...(gibson.settings.tenant ? { tenant: gibson.settings.tenant } : {}),
|
|
47
|
+
...(gibson.settings.targetId ? { target_id: gibson.settings.targetId } : {}),
|
|
48
|
+
...(gibson.live
|
|
49
|
+
? {
|
|
50
|
+
mission_id: gibson.live.missionId,
|
|
51
|
+
work_id: gibson.live.workId,
|
|
52
|
+
callback_endpoint: gibson.live.harness.endpoint,
|
|
53
|
+
callback_insecure: gibson.settings.callbackInsecure,
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
56
|
+
...(gibson.runId ? { run_id: gibson.runId } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function resources(gibson, ambient) {
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
uri: AMBIENT_URI,
|
|
63
|
+
name: "gibson_ambient",
|
|
64
|
+
title: "Gibson ambient knowledge",
|
|
65
|
+
description: "One GraphRAG lookup for this session: the memories, prior findings and security facts the " +
|
|
66
|
+
"tenant already holds about this codebase. Read it once at the start of a session. Empty " +
|
|
67
|
+
"when the session has no platform.",
|
|
68
|
+
mimeType: "text/markdown",
|
|
69
|
+
read: async () => ({ uri: AMBIENT_URI, mimeType: "text/markdown", text: await ambient.block() }),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
uri: SESSION_URI,
|
|
73
|
+
name: "gibson_session",
|
|
74
|
+
title: "Gibson session coordinates",
|
|
75
|
+
description: "How this session is connected: the check-in source, the posture, and the mission, run and " +
|
|
76
|
+
"callback endpoint a session-end hook needs to checkpoint the transcript.",
|
|
77
|
+
mimeType: "application/json",
|
|
78
|
+
read: async () => ({ uri: SESSION_URI, mimeType: "application/json", text: `${JSON.stringify(sessionCoordinates(gibson()), null, 2)}\n` }),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
/** The ambient block as a prompt, for a host with no hook surface. */
|
|
83
|
+
export function ambientPrompt(ambient) {
|
|
84
|
+
return {
|
|
85
|
+
name: "gibson_ambient",
|
|
86
|
+
title: "Gibson prior context",
|
|
87
|
+
description: "Load what this tenant's Gibson graph already knows about this codebase. Use it at the start of a session, or with a question of your own.",
|
|
88
|
+
arguments: [{ name: "query", description: "What to look for. Defaults to the session's opening lookup.", required: false }],
|
|
89
|
+
get: async (args) => {
|
|
90
|
+
const query = z.string().optional().parse(args.query);
|
|
91
|
+
const block = await ambient.block(query);
|
|
92
|
+
return {
|
|
93
|
+
description: "Prior context from the Gibson knowledge graph",
|
|
94
|
+
messages: [{ role: "user", content: { type: "text", text: block || "The Gibson knowledge graph holds nothing for this codebase yet." } }],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type DescMethod, type DescService } from "@bufbuild/protobuf";
|
|
2
|
+
import { type Transport } from "@connectrpc/connect";
|
|
3
|
+
import type { TaskHarness } from "@zeroroot-ai/sdk";
|
|
4
|
+
import type { ToolDefinition } from "./registry.js";
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* Naming is `<service>_<method>` in snake case, e.g.
|
|
11
|
+
* `harness_callback_service_world_view`. The service prefix is what keeps
|
|
12
|
+
* these apart from the helper tools, which are named after the helper
|
|
13
|
+
* (`world_view`, `remember`, `submit_finding`).
|
|
14
|
+
*/
|
|
15
|
+
/** Where an RPC is reached. */
|
|
16
|
+
export interface RpcChannels {
|
|
17
|
+
/**
|
|
18
|
+
* The component's own transport, from the check-in. Absent in a dispatched
|
|
19
|
+
* run, which holds only a task grant.
|
|
20
|
+
*/
|
|
21
|
+
session?: Transport;
|
|
22
|
+
/** The task-scoped callback harness. Absent in the component posture. */
|
|
23
|
+
task?: TaskHarness;
|
|
24
|
+
}
|
|
25
|
+
/** The fully-qualified name of the context message every callback RPC carries. */
|
|
26
|
+
export declare const CONTEXT_INFO = "gibson.harness.v1.ContextInfo";
|
|
27
|
+
/**
|
|
28
|
+
* `LLMCompleteWithTools` -> `llm_complete_with_tools`. An acronym run stays
|
|
29
|
+
* one word, so `LLMComplete` is not `l_l_m_complete`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function snake(name: string): string;
|
|
32
|
+
export declare function toolNameFor(service: DescService, method: DescMethod): string;
|
|
33
|
+
/**
|
|
34
|
+
* Which transport an RPC rides.
|
|
35
|
+
*
|
|
36
|
+
* A `gibson.harness.v1` RPC is a callback: it belongs on the task harness,
|
|
37
|
+
* under the grant of the dispatch it serves. Everything else is the daemon's
|
|
38
|
+
* own surface and rides the component transport. Either one may be absent,
|
|
39
|
+
* so each falls back to the other rather than losing the tool: a dispatched
|
|
40
|
+
* run has no component transport, and the callback endpoint is the same
|
|
41
|
+
* daemon.
|
|
42
|
+
*/
|
|
43
|
+
export declare function transportFor(service: DescService, channels: RpcChannels): Transport | undefined;
|
|
44
|
+
export interface RpcToolOptions {
|
|
45
|
+
channels: RpcChannels;
|
|
46
|
+
/** Messages a server-streaming RPC returns before it truncates. */
|
|
47
|
+
streamLimit: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build every RPC tool that has a transport to ride.
|
|
51
|
+
*
|
|
52
|
+
* A posture with neither transport (standalone) gets none: an RPC tool with
|
|
53
|
+
* no daemon behind it would answer every call with a dial error, which reads
|
|
54
|
+
* to a model like a broken platform rather than an unconnected session.
|
|
55
|
+
*/
|
|
56
|
+
export declare function rpcTools(opts: RpcToolOptions): ToolDefinition[];
|
|
57
|
+
/** An empty request message, for a test that needs one. */
|
|
58
|
+
export declare function emptyRequest(method: DescMethod): unknown;
|
|
59
|
+
/** What the generated table says about one service. */
|
|
60
|
+
export interface ServiceDoc {
|
|
61
|
+
service: DescService;
|
|
62
|
+
methods: {
|
|
63
|
+
method: string;
|
|
64
|
+
description: string;
|
|
65
|
+
}[];
|
|
66
|
+
}
|
|
67
|
+
/** What a drift comparison found. Empty on both sides means no drift. */
|
|
68
|
+
export interface Drift {
|
|
69
|
+
/** RPCs the descriptors have and the table does not. */
|
|
70
|
+
missing: string[];
|
|
71
|
+
/** Entries the table has and the descriptors do not. */
|
|
72
|
+
extra: string[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compare a generated table against the descriptors it claims to describe.
|
|
76
|
+
*
|
|
77
|
+
* The guard and its test both call this, so what CI enforces and what the
|
|
78
|
+
* test proves are one function rather than two spellings of one intention.
|
|
79
|
+
*/
|
|
80
|
+
export declare function driftBetween(table: ServiceDoc[]): Drift;
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { create, fromJson, toJson } from "@bufbuild/protobuf";
|
|
2
|
+
import { createClient } from "@connectrpc/connect";
|
|
3
|
+
import { GENERATED_SERVICES } from "./generated/tools.js";
|
|
4
|
+
import { requestSchema } from "./schema.js";
|
|
5
|
+
import { failure, json } from "./tools/result.js";
|
|
6
|
+
/** The fully-qualified name of the context message every callback RPC carries. */
|
|
7
|
+
export const CONTEXT_INFO = "gibson.harness.v1.ContextInfo";
|
|
8
|
+
/** The proto package whose services are served on the callback endpoint. */
|
|
9
|
+
const CALLBACK_PACKAGE = "gibson.harness.v1.";
|
|
10
|
+
/**
|
|
11
|
+
* `LLMCompleteWithTools` -> `llm_complete_with_tools`. An acronym run stays
|
|
12
|
+
* one word, so `LLMComplete` is not `l_l_m_complete`.
|
|
13
|
+
*/
|
|
14
|
+
export function snake(name) {
|
|
15
|
+
return name
|
|
16
|
+
.replace(/([A-Za-z0-9])([A-Z][a-z])/g, "$1_$2")
|
|
17
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
18
|
+
.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
export function toolNameFor(service, method) {
|
|
21
|
+
return `${snake(service.name)}_${snake(method.name)}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Which transport an RPC rides.
|
|
25
|
+
*
|
|
26
|
+
* A `gibson.harness.v1` RPC is a callback: it belongs on the task harness,
|
|
27
|
+
* under the grant of the dispatch it serves. Everything else is the daemon's
|
|
28
|
+
* own surface and rides the component transport. Either one may be absent,
|
|
29
|
+
* so each falls back to the other rather than losing the tool: a dispatched
|
|
30
|
+
* run has no component transport, and the callback endpoint is the same
|
|
31
|
+
* daemon.
|
|
32
|
+
*/
|
|
33
|
+
export function transportFor(service, channels) {
|
|
34
|
+
const callback = service.typeName.startsWith(CALLBACK_PACKAGE);
|
|
35
|
+
const preferred = callback ? channels.task?.transport : channels.session;
|
|
36
|
+
return preferred ?? (callback ? channels.session : channels.task?.transport);
|
|
37
|
+
}
|
|
38
|
+
/** The `context` field a callback request carries, when it has one. */
|
|
39
|
+
function contextField(input) {
|
|
40
|
+
const field = input.fields.find((f) => f.fieldKind === "message" && f.message.typeName === CONTEXT_INFO);
|
|
41
|
+
return field?.jsonName;
|
|
42
|
+
}
|
|
43
|
+
function describe(service, method, comment, streamLimit) {
|
|
44
|
+
const head = `${service.name}.${method.name}`;
|
|
45
|
+
const body = comment || `No comment in the proto for this RPC of ${service.typeName}.`;
|
|
46
|
+
const tail = method.methodKind === "server_streaming"
|
|
47
|
+
? ` Server-streaming: returns up to ${streamLimit} messages as a JSON array, with truncated: true when the limit was reached.`
|
|
48
|
+
: method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming"
|
|
49
|
+
? " Takes an array of request messages in `requests`."
|
|
50
|
+
: "";
|
|
51
|
+
return `${head}: ${body}${tail}`;
|
|
52
|
+
}
|
|
53
|
+
/** The input schema, which for a streaming-request RPC wraps the messages in an array. */
|
|
54
|
+
function inputSchemaFor(method) {
|
|
55
|
+
const body = requestSchema(method.input);
|
|
56
|
+
if (method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming") {
|
|
57
|
+
return {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: { requests: { type: "array", items: body, description: `Request messages of type ${method.input.typeName}.` } },
|
|
60
|
+
required: ["requests"],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
async function collect(stream, output, limit) {
|
|
66
|
+
const messages = [];
|
|
67
|
+
for await (const message of stream) {
|
|
68
|
+
if (messages.length >= limit)
|
|
69
|
+
return { messages, truncated: true };
|
|
70
|
+
messages.push(toJson(output, message));
|
|
71
|
+
}
|
|
72
|
+
return { messages, truncated: false };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build every RPC tool that has a transport to ride.
|
|
76
|
+
*
|
|
77
|
+
* A posture with neither transport (standalone) gets none: an RPC tool with
|
|
78
|
+
* no daemon behind it would answer every call with a dial error, which reads
|
|
79
|
+
* to a model like a broken platform rather than an unconnected session.
|
|
80
|
+
*/
|
|
81
|
+
export function rpcTools(opts) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const entry of GENERATED_SERVICES) {
|
|
84
|
+
const service = entry.service;
|
|
85
|
+
const transport = transportFor(service, opts.channels);
|
|
86
|
+
if (!transport)
|
|
87
|
+
continue;
|
|
88
|
+
const comments = new Map(entry.methods.map((m) => [m.method, m.description]));
|
|
89
|
+
// The descriptor is the source of truth for what exists; the generated
|
|
90
|
+
// table only supplies the prose. A method the table has not seen still
|
|
91
|
+
// becomes a tool, and the drift test is what says the table is stale.
|
|
92
|
+
const client = createClient(service, transport);
|
|
93
|
+
for (const method of service.methods) {
|
|
94
|
+
out.push(rpcTool(service, method, client, comments.get(method.localName) ?? "", opts));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function rpcTool(service, method, client, comment, opts) {
|
|
100
|
+
const contextKey = contextField(method.input);
|
|
101
|
+
const streaming = method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming";
|
|
102
|
+
return {
|
|
103
|
+
name: toolNameFor(service, method),
|
|
104
|
+
description: describe(service, method, comment, opts.streamLimit),
|
|
105
|
+
inputSchema: inputSchemaFor(method),
|
|
106
|
+
handler: async (args, ctx) => decode(service, method, client, contextKey, streaming, args, ctx, opts),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function decode(service, method, client, contextKey, streaming, args, ctx, opts) {
|
|
110
|
+
const fill = (raw) => {
|
|
111
|
+
// Every callback RPC resolves the harness from ContextInfo and refuses a
|
|
112
|
+
// request without one. The grant already names the mission and the task,
|
|
113
|
+
// so the server fills it when the caller left it out; a caller that sets
|
|
114
|
+
// it keeps what it wrote.
|
|
115
|
+
if (!contextKey || raw[contextKey] || !opts.channels.task)
|
|
116
|
+
return raw;
|
|
117
|
+
return { ...raw, [contextKey]: { ...opts.channels.task.context } };
|
|
118
|
+
};
|
|
119
|
+
let messages;
|
|
120
|
+
try {
|
|
121
|
+
if (streaming) {
|
|
122
|
+
const list = args.requests;
|
|
123
|
+
if (!Array.isArray(list))
|
|
124
|
+
return failure(`${method.name} needs requests`, "This RPC takes a stream of requests. Pass them as an array in `requests`.");
|
|
125
|
+
messages = list.map((raw) => fromJson(method.input, fill(raw)));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
messages = [fromJson(method.input, fill(args))];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
return failure(`${method.name}: invalid request`, `${e.message}\n\nThe request must be canonical protojson for ${method.input.typeName}.`);
|
|
133
|
+
}
|
|
134
|
+
const call = client[method.localName];
|
|
135
|
+
if (!call)
|
|
136
|
+
return failure(`${method.name} is unavailable`, `The generated client for ${service.typeName} has no ${method.localName}.`);
|
|
137
|
+
const options = { signal: ctx.signal };
|
|
138
|
+
if (method.methodKind === "unary") {
|
|
139
|
+
const res = await call(messages[0], options);
|
|
140
|
+
return json(toJson(method.output, res));
|
|
141
|
+
}
|
|
142
|
+
if (method.methodKind === "server_streaming") {
|
|
143
|
+
const { messages: out, truncated } = await collect(call(messages[0], options), method.output, opts.streamLimit);
|
|
144
|
+
return json({ messages: out, truncated });
|
|
145
|
+
}
|
|
146
|
+
// Client-streaming and bidi both take the requests as an async iterable.
|
|
147
|
+
const source = (async function* () {
|
|
148
|
+
for (const m of messages)
|
|
149
|
+
yield m;
|
|
150
|
+
})();
|
|
151
|
+
if (method.methodKind === "client_streaming") {
|
|
152
|
+
const res = await call(source, options);
|
|
153
|
+
return json(toJson(method.output, res));
|
|
154
|
+
}
|
|
155
|
+
const { messages: out, truncated } = await collect(call(source, options), method.output, opts.streamLimit);
|
|
156
|
+
return json({ messages: out, truncated });
|
|
157
|
+
}
|
|
158
|
+
/** An empty request message, for a test that needs one. */
|
|
159
|
+
export function emptyRequest(method) {
|
|
160
|
+
return create(method.input);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Compare a generated table against the descriptors it claims to describe.
|
|
164
|
+
*
|
|
165
|
+
* The guard and its test both call this, so what CI enforces and what the
|
|
166
|
+
* test proves are one function rather than two spellings of one intention.
|
|
167
|
+
*/
|
|
168
|
+
export function driftBetween(table) {
|
|
169
|
+
const missing = [];
|
|
170
|
+
const extra = [];
|
|
171
|
+
for (const entry of table) {
|
|
172
|
+
const declared = new Set(entry.methods.map((m) => m.method));
|
|
173
|
+
const real = new Set(entry.service.methods.map((m) => m.localName));
|
|
174
|
+
for (const name of real) {
|
|
175
|
+
if (!declared.has(name))
|
|
176
|
+
missing.push(`${entry.service.typeName}.${name}`);
|
|
177
|
+
}
|
|
178
|
+
for (const name of declared) {
|
|
179
|
+
if (!real.has(name))
|
|
180
|
+
extra.push(`${entry.service.typeName}.${name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { missing: missing.sort(), extra: extra.sort() };
|
|
184
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ScalarType, type DescEnum, type DescMessage } from "@bufbuild/protobuf";
|
|
2
|
+
import type { JsonSchema } from "./registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* A JSON Schema for a proto request message, walked from its descriptor.
|
|
5
|
+
*
|
|
6
|
+
* The schema describes canonical protojson, because that is what the tool
|
|
7
|
+
* decodes with `fromJson`: camelCase keys, 64-bit integers as decimal
|
|
8
|
+
* strings, bytes as base64, enums as their value names, timestamps as
|
|
9
|
+
* RFC3339. `fromJson` also accepts the proto field name and a bare number
|
|
10
|
+
* for an enum, so a model that writes either still succeeds; the schema
|
|
11
|
+
* names one spelling so the model has one to copy.
|
|
12
|
+
*
|
|
13
|
+
* The walk is bounded. A message that reaches {@link MAX_DEPTH} or that
|
|
14
|
+
* appears twice on the path becomes a bare object with its proto type named
|
|
15
|
+
* in the description. Without a bound, a self-referential message (a graph
|
|
16
|
+
* node, a mission definition) has no finite schema, and the daemon's larger
|
|
17
|
+
* request types would each produce hundreds of kilobytes that every
|
|
18
|
+
* `tools/list` then carries.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_DEPTH = 5;
|
|
21
|
+
export declare function scalarSchema(t: ScalarType): Record<string, unknown>;
|
|
22
|
+
export declare function enumSchema(e: DescEnum): Record<string, unknown>;
|
|
23
|
+
export declare function messageSchema(desc: DescMessage, path?: string[]): Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* The tool input schema for a request message.
|
|
26
|
+
*
|
|
27
|
+
* `additionalProperties` stays open at the top level: `fromJson` runs with
|
|
28
|
+
* `ignoreUnknownFields` off, so a wrong key is refused with the server's own
|
|
29
|
+
* wording rather than by a schema check that says less.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requestSchema(desc: DescMessage): JsonSchema;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ScalarType } from "@bufbuild/protobuf";
|
|
2
|
+
/**
|
|
3
|
+
* A JSON Schema for a proto request message, walked from its descriptor.
|
|
4
|
+
*
|
|
5
|
+
* The schema describes canonical protojson, because that is what the tool
|
|
6
|
+
* decodes with `fromJson`: camelCase keys, 64-bit integers as decimal
|
|
7
|
+
* strings, bytes as base64, enums as their value names, timestamps as
|
|
8
|
+
* RFC3339. `fromJson` also accepts the proto field name and a bare number
|
|
9
|
+
* for an enum, so a model that writes either still succeeds; the schema
|
|
10
|
+
* names one spelling so the model has one to copy.
|
|
11
|
+
*
|
|
12
|
+
* The walk is bounded. A message that reaches {@link MAX_DEPTH} or that
|
|
13
|
+
* appears twice on the path becomes a bare object with its proto type named
|
|
14
|
+
* in the description. Without a bound, a self-referential message (a graph
|
|
15
|
+
* node, a mission definition) has no finite schema, and the daemon's larger
|
|
16
|
+
* request types would each produce hundreds of kilobytes that every
|
|
17
|
+
* `tools/list` then carries.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_DEPTH = 5;
|
|
20
|
+
/** Proto types with a JSON form of their own, which no field walk can describe. */
|
|
21
|
+
const WELL_KNOWN = {
|
|
22
|
+
"google.protobuf.Timestamp": { type: "string", format: "date-time", description: "RFC 3339 timestamp, e.g. 2026-09-01T12:00:00Z." },
|
|
23
|
+
"google.protobuf.Duration": { type: "string", description: 'Seconds with up to nine fractional digits, ending in "s", e.g. "1.5s".' },
|
|
24
|
+
"google.protobuf.Struct": { type: "object", description: "Free-form JSON object." },
|
|
25
|
+
"google.protobuf.Value": { description: "Any JSON value." },
|
|
26
|
+
"google.protobuf.ListValue": { type: "array", description: "Any JSON array." },
|
|
27
|
+
"google.protobuf.Any": { type: "object", description: 'A packed message: {"@type": "type.googleapis.com/<proto type>", ...fields}.' },
|
|
28
|
+
"google.protobuf.FieldMask": { type: "string", description: "Comma-separated field paths." },
|
|
29
|
+
"google.protobuf.Empty": { type: "object", description: "No fields." },
|
|
30
|
+
"google.protobuf.BoolValue": { type: "boolean" },
|
|
31
|
+
"google.protobuf.StringValue": { type: "string" },
|
|
32
|
+
"google.protobuf.BytesValue": { type: "string", contentEncoding: "base64" },
|
|
33
|
+
"google.protobuf.DoubleValue": { type: "number" },
|
|
34
|
+
"google.protobuf.FloatValue": { type: "number" },
|
|
35
|
+
"google.protobuf.Int32Value": { type: "integer" },
|
|
36
|
+
"google.protobuf.UInt32Value": { type: "integer" },
|
|
37
|
+
"google.protobuf.Int64Value": { type: "string", description: "64-bit integer as a decimal string." },
|
|
38
|
+
"google.protobuf.UInt64Value": { type: "string", description: "64-bit integer as a decimal string." },
|
|
39
|
+
// A TypedValue is a oneof over eight kinds, and the daemon carries them in
|
|
40
|
+
// free-form maps. Any JSON value is the honest description.
|
|
41
|
+
"gibson.common.v1.TypedValue": { description: "Any JSON value: string, number, boolean, null, array or object." },
|
|
42
|
+
};
|
|
43
|
+
export function scalarSchema(t) {
|
|
44
|
+
switch (t) {
|
|
45
|
+
case ScalarType.DOUBLE:
|
|
46
|
+
case ScalarType.FLOAT:
|
|
47
|
+
return { type: "number" };
|
|
48
|
+
case ScalarType.INT32:
|
|
49
|
+
case ScalarType.UINT32:
|
|
50
|
+
case ScalarType.SINT32:
|
|
51
|
+
case ScalarType.FIXED32:
|
|
52
|
+
case ScalarType.SFIXED32:
|
|
53
|
+
return { type: "integer" };
|
|
54
|
+
case ScalarType.INT64:
|
|
55
|
+
case ScalarType.UINT64:
|
|
56
|
+
case ScalarType.SINT64:
|
|
57
|
+
case ScalarType.FIXED64:
|
|
58
|
+
case ScalarType.SFIXED64:
|
|
59
|
+
return { type: "string", description: "64-bit integer as a decimal string. A JSON number is also accepted." };
|
|
60
|
+
case ScalarType.BOOL:
|
|
61
|
+
return { type: "boolean" };
|
|
62
|
+
case ScalarType.STRING:
|
|
63
|
+
return { type: "string" };
|
|
64
|
+
case ScalarType.BYTES:
|
|
65
|
+
return { type: "string", contentEncoding: "base64", description: "Base64-encoded bytes." };
|
|
66
|
+
default:
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function enumSchema(e) {
|
|
71
|
+
return {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: e.values.map((v) => v.name),
|
|
74
|
+
description: `One of ${e.typeName}. The number is also accepted.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function singular(field, path) {
|
|
78
|
+
switch (field.fieldKind) {
|
|
79
|
+
case "scalar":
|
|
80
|
+
return scalarSchema(field.scalar);
|
|
81
|
+
case "enum":
|
|
82
|
+
return enumSchema(field.enum);
|
|
83
|
+
case "message":
|
|
84
|
+
return messageSchema(field.message, path);
|
|
85
|
+
default:
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function fieldSchema(field, path) {
|
|
90
|
+
switch (field.fieldKind) {
|
|
91
|
+
case "list": {
|
|
92
|
+
const items = field.listKind === "scalar"
|
|
93
|
+
? scalarSchema(field.scalar)
|
|
94
|
+
: field.listKind === "enum"
|
|
95
|
+
? enumSchema(field.enum)
|
|
96
|
+
: messageSchema(field.message, path);
|
|
97
|
+
return { type: "array", items };
|
|
98
|
+
}
|
|
99
|
+
case "map": {
|
|
100
|
+
const values = field.mapKind === "scalar"
|
|
101
|
+
? scalarSchema(field.scalar)
|
|
102
|
+
: field.mapKind === "enum"
|
|
103
|
+
? enumSchema(field.enum)
|
|
104
|
+
: messageSchema(field.message, path);
|
|
105
|
+
// protojson writes every map key as a string, whatever the proto key type.
|
|
106
|
+
return { type: "object", additionalProperties: values };
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
return singular(field, path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function messageSchema(desc, path = []) {
|
|
113
|
+
const known = WELL_KNOWN[desc.typeName];
|
|
114
|
+
if (known)
|
|
115
|
+
return { ...known };
|
|
116
|
+
if (path.includes(desc.typeName)) {
|
|
117
|
+
return { type: "object", description: `${desc.typeName}, which contains itself. Pass its protojson form.` };
|
|
118
|
+
}
|
|
119
|
+
if (path.length >= MAX_DEPTH) {
|
|
120
|
+
return { type: "object", description: `${desc.typeName}, nested past depth ${MAX_DEPTH}. Pass its protojson form.` };
|
|
121
|
+
}
|
|
122
|
+
const next = [...path, desc.typeName];
|
|
123
|
+
const properties = {};
|
|
124
|
+
for (const field of desc.fields) {
|
|
125
|
+
const schema = fieldSchema(field, next);
|
|
126
|
+
const oneof = field.oneof ? `One of the ${field.oneof.name} group; set at most one of them.` : "";
|
|
127
|
+
const existing = typeof schema.description === "string" ? schema.description : "";
|
|
128
|
+
const description = [existing, oneof].filter(Boolean).join(" ");
|
|
129
|
+
properties[field.jsonName] = description ? { ...schema, description } : schema;
|
|
130
|
+
}
|
|
131
|
+
return { type: "object", properties, additionalProperties: false };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The tool input schema for a request message.
|
|
135
|
+
*
|
|
136
|
+
* `additionalProperties` stays open at the top level: `fromJson` runs with
|
|
137
|
+
* `ignoreUnknownFields` off, so a wrong key is refused with the server's own
|
|
138
|
+
* wording rather than by a schema check that says less.
|
|
139
|
+
*/
|
|
140
|
+
export function requestSchema(desc) {
|
|
141
|
+
const body = messageSchema(desc);
|
|
142
|
+
return { ...body, type: "object" };
|
|
143
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import type { ToolRegistry } from "./registry.js";
|
|
4
|
+
import type { PromptDefinition, ResourceDefinition } from "./resources.js";
|
|
5
|
+
/** The package version, read from package.json beside dist. */
|
|
6
|
+
export declare function packageVersion(): string;
|
|
7
|
+
export declare const SERVER_NAME = "gibson";
|
|
8
|
+
export interface ServerBinding {
|
|
9
|
+
registry: ToolRegistry;
|
|
10
|
+
/** Text the host shows the model once, at initialize. */
|
|
11
|
+
instructions?: string;
|
|
12
|
+
/** Readable resources: the ambient block and the session coordinates. */
|
|
13
|
+
resources?: ResourceDefinition[];
|
|
14
|
+
/** Prompts, for a host with no hook surface to inject the ambient block. */
|
|
15
|
+
prompts?: PromptDefinition[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Bind one MCP protocol server to a transport. Every server reads the same
|
|
19
|
+
* registry: the stdio path has one, the HTTP path has one per session. A
|
|
20
|
+
* registry change reaches each attached server as `tools/list_changed`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function attachServer(binding: ServerBinding, transport: Transport): Promise<Server>;
|