@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,24 @@
|
|
|
1
|
+
import { componentizeTools } from "./componentize.js";
|
|
2
|
+
import { helperContext } from "./context.js";
|
|
3
|
+
import { delegationTools } from "./delegate.js";
|
|
4
|
+
import { findingTools } from "./findings.js";
|
|
5
|
+
import { knowledgeTools } from "./knowledge.js";
|
|
6
|
+
import { platformToolHelpers } from "./tools.js";
|
|
7
|
+
export * from "./context.js";
|
|
8
|
+
export * from "./coverage.js";
|
|
9
|
+
export { componentizeTools } from "./componentize.js";
|
|
10
|
+
export { delegationTools } from "./delegate.js";
|
|
11
|
+
export { findingTools, gibsonFindingsBackend, localFindingsBackend, taskFindingsBackend } from "./findings.js";
|
|
12
|
+
export { knowledgeTools } from "./knowledge.js";
|
|
13
|
+
export { platformToolHelpers } from "./tools.js";
|
|
14
|
+
/**
|
|
15
|
+
* One tool per SDK helper (gibson#1706, decision 2), for whatever this
|
|
16
|
+
* posture can reach. A helper whose grant is absent registers no tool
|
|
17
|
+
* rather than a tool that always fails.
|
|
18
|
+
*/
|
|
19
|
+
export function helperTools(ctx) {
|
|
20
|
+
return [...findingTools(ctx), ...knowledgeTools(ctx), ...delegationTools(ctx), ...componentizeTools(ctx), ...platformToolHelpers(ctx)];
|
|
21
|
+
}
|
|
22
|
+
export function helperToolsFor(gibson, cwd, env) {
|
|
23
|
+
return helperTools(helperContext(gibson, cwd, env));
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../registry.js";
|
|
2
|
+
import type { HelperContext } from "./context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Memory and knowledge (gibson#1593, decisions 9 and 10).
|
|
5
|
+
*
|
|
6
|
+
* `remember` is `Observe(MemoryObservation)`: the memory lands in the tenant
|
|
7
|
+
* World as a Timeline event and the projector writes the graph node. It
|
|
8
|
+
* exists only where a mission is open, because the World is written under a
|
|
9
|
+
* mission.
|
|
10
|
+
*
|
|
11
|
+
* `recall` reads the graph. Under a task grant it reads as the task, as a
|
|
12
|
+
* checked-in component it reads as the component. The model decides when a
|
|
13
|
+
* lookup is worth a round trip, and the query it writes beats anything
|
|
14
|
+
* derived from the prompt.
|
|
15
|
+
*/
|
|
16
|
+
export declare function knowledgeTools(ctx: HelperContext): ToolDefinition[];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { formatKnowledgeForPrompt, isSeamUnavailable, observe, remember, WorldEntityKind } from "@zeroroot-ai/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineTool } from "../tool.js";
|
|
4
|
+
import { failure, json, text } from "../tools/result.js";
|
|
5
|
+
/**
|
|
6
|
+
* Memory and knowledge (gibson#1593, decisions 9 and 10).
|
|
7
|
+
*
|
|
8
|
+
* `remember` is `Observe(MemoryObservation)`: the memory lands in the tenant
|
|
9
|
+
* World as a Timeline event and the projector writes the graph node. It
|
|
10
|
+
* exists only where a mission is open, because the World is written under a
|
|
11
|
+
* mission.
|
|
12
|
+
*
|
|
13
|
+
* `recall` reads the graph. Under a task grant it reads as the task, as a
|
|
14
|
+
* checked-in component it reads as the component. The model decides when a
|
|
15
|
+
* lookup is worth a round trip, and the query it writes beats anything
|
|
16
|
+
* derived from the prompt.
|
|
17
|
+
*/
|
|
18
|
+
export function knowledgeTools(ctx) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const { knowledge, live } = ctx.gibson;
|
|
21
|
+
if (knowledge) {
|
|
22
|
+
out.push(defineTool({
|
|
23
|
+
name: "recall",
|
|
24
|
+
description: "Search this tenant's Gibson knowledge graph for memories, prior findings, targets and " +
|
|
25
|
+
"security facts recorded by earlier sessions and runs. Use it before you start on an " +
|
|
26
|
+
"unfamiliar area, or to check whether something was already recorded.",
|
|
27
|
+
input: {
|
|
28
|
+
query: z.string().describe("What to look for, in natural language."),
|
|
29
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum hits. Defaults to 10."),
|
|
30
|
+
node_types: z.array(z.string()).optional().describe('Restrict to node types, e.g. ["Observation"] for memories or ["Finding"].'),
|
|
31
|
+
},
|
|
32
|
+
annotations: { readOnlyHint: true },
|
|
33
|
+
handler: async (args) => {
|
|
34
|
+
let hits;
|
|
35
|
+
try {
|
|
36
|
+
hits = await knowledge.query({ text: args.query, topK: args.limit ?? 10, ...(args.node_types ? { nodeTypes: args.node_types } : {}) });
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (isSeamUnavailable(e)) {
|
|
40
|
+
return text("recall unavailable", "The Gibson knowledge graph is not available on this daemon (gibson#1186). Continue without prior context.");
|
|
41
|
+
}
|
|
42
|
+
return failure("recall failed", e.message);
|
|
43
|
+
}
|
|
44
|
+
if (hits.length === 0)
|
|
45
|
+
return text("no matches", `Nothing in the tenant graph matches "${args.query}".`);
|
|
46
|
+
return text(`${hits.length} match${hits.length === 1 ? "" : "es"}`, formatKnowledgeForPrompt(hits));
|
|
47
|
+
},
|
|
48
|
+
}), defineTool({
|
|
49
|
+
name: "similar_findings",
|
|
50
|
+
description: "Findings in this tenant's graph that are semantically similar to a known finding.",
|
|
51
|
+
input: { finding_id: z.string().describe("The finding to search from."), limit: z.number().int().min(1).max(50).optional().describe("Maximum hits. Defaults to 5.") },
|
|
52
|
+
annotations: { readOnlyHint: true },
|
|
53
|
+
handler: async (args) => json(await knowledge.similarFindings(args.finding_id, args.limit ?? 5)),
|
|
54
|
+
}), defineTool({
|
|
55
|
+
name: "related_findings",
|
|
56
|
+
description: "Findings reachable from a given finding through the graph's edges.",
|
|
57
|
+
input: { finding_id: z.string().describe("The finding to traverse from.") },
|
|
58
|
+
annotations: { readOnlyHint: true },
|
|
59
|
+
handler: async (args) => json(await knowledge.relatedFindings(args.finding_id)),
|
|
60
|
+
}), defineTool({
|
|
61
|
+
name: "similar_attacks",
|
|
62
|
+
description: "Attack patterns in the graph that are semantically similar to a piece of content.",
|
|
63
|
+
input: { content: z.string().describe("Text to match attack patterns against."), limit: z.number().int().min(1).max(50).optional().describe("Maximum hits. Defaults to 5.") },
|
|
64
|
+
annotations: { readOnlyHint: true },
|
|
65
|
+
handler: async (args) => json(await knowledge.similarAttacks(args.content, args.limit ?? 5)),
|
|
66
|
+
}), defineTool({
|
|
67
|
+
name: "attack_chains",
|
|
68
|
+
description: "Multi-hop attack paths that start from a MITRE technique.",
|
|
69
|
+
input: { technique_id: z.string().describe("MITRE technique id, e.g. T1190."), max_depth: z.number().int().min(1).max(10).optional().describe("Hops to follow. Defaults to 3.") },
|
|
70
|
+
annotations: { readOnlyHint: true },
|
|
71
|
+
handler: async (args) => json(await knowledge.attackChains(args.technique_id, args.max_depth ?? 3)),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
if (live) {
|
|
75
|
+
const harness = live.harness;
|
|
76
|
+
out.push(defineTool({
|
|
77
|
+
name: "remember",
|
|
78
|
+
description: "Keep a fact for later sessions, in this tenant's Gibson knowledge graph. Use it for a " +
|
|
79
|
+
"convention, a decision, a layout fact or a gotcha you would want back next time. Not for " +
|
|
80
|
+
"progress notes. A security finding goes to submit_finding instead.",
|
|
81
|
+
input: {
|
|
82
|
+
text: z.string().describe("The fact, in one or two sentences."),
|
|
83
|
+
kind: z.string().optional().describe('A short category: "convention", "decision", "layout", "gotcha".'),
|
|
84
|
+
tags: z.array(z.string()).optional().describe("Lower-case labels that help recall."),
|
|
85
|
+
source_ref: z.string().optional().describe("Where it came from: a path, a URL, or a work id."),
|
|
86
|
+
},
|
|
87
|
+
handler: async (args) => {
|
|
88
|
+
try {
|
|
89
|
+
await remember(harness, { text: args.text, kind: args.kind, tags: args.tags, sourceRef: args.source_ref });
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return failure("remember failed", e.message);
|
|
93
|
+
}
|
|
94
|
+
return text("remembered", `Recorded in the tenant graph under mission ${live.missionId}.`);
|
|
95
|
+
},
|
|
96
|
+
}), defineTool({
|
|
97
|
+
name: "observe",
|
|
98
|
+
description: "Write a typed observation to the tenant World: an entity the platform should know about, " +
|
|
99
|
+
"an edge between two of them, or a lifecycle event. Use remember for a plain fact and " +
|
|
100
|
+
"submit_finding for a vulnerability.",
|
|
101
|
+
input: { observation: z.record(z.string(), z.unknown()).describe("An Observation object: a memory, an entity, an edge, or a lifecycle entity.") },
|
|
102
|
+
handler: async (args) => {
|
|
103
|
+
try {
|
|
104
|
+
await observe(harness, args.observation);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return failure("observe failed", e.message);
|
|
108
|
+
}
|
|
109
|
+
return text("observed", `Recorded in the World under mission ${live.missionId}.`);
|
|
110
|
+
},
|
|
111
|
+
}), defineTool({
|
|
112
|
+
name: "world_view",
|
|
113
|
+
description: "Read this mission's slice of the tenant World: the hosts, domains, credentials, accounts " +
|
|
114
|
+
"and findings the platform knows about the target. Pass handles from a previous call in " +
|
|
115
|
+
"focus to zoom into entities.",
|
|
116
|
+
input: { focus: z.array(z.string()).optional().describe("Entity handles from an earlier world_view call.") },
|
|
117
|
+
annotations: { readOnlyHint: true },
|
|
118
|
+
handler: async (args) => {
|
|
119
|
+
try {
|
|
120
|
+
const res = await harness.client.worldView({ context: harness.context, focus: args.focus ?? [] });
|
|
121
|
+
if (res.error)
|
|
122
|
+
return failure("world_view refused", res.error.message);
|
|
123
|
+
if (res.entities.length === 0)
|
|
124
|
+
return text("empty slice", "The World holds nothing for this target yet.");
|
|
125
|
+
const lines = res.entities.map((e) => {
|
|
126
|
+
const attrs = Object.entries(e.attributes).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
127
|
+
return `- [${e.handle}] ${WorldEntityKind[e.kind] ?? e.kind} ${e.label}${attrs ? ` (${attrs})` : ""}`;
|
|
128
|
+
});
|
|
129
|
+
return text(`${res.entities.length} entit${res.entities.length === 1 ? "y" : "ies"}${res.truncated ? " (truncated)" : ""}`, lines.join("\n"));
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
return failure("world_view failed", e.message);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
}), defineTool({
|
|
136
|
+
name: "run_history",
|
|
137
|
+
description: "Earlier runs of this mission's target, so a session can see what previous runs already did.",
|
|
138
|
+
input: {},
|
|
139
|
+
annotations: { readOnlyHint: true },
|
|
140
|
+
handler: async () => json(await (ctx.gibson.knowledge ?? knowledge).runHistory()),
|
|
141
|
+
}), defineTool({
|
|
142
|
+
name: "application_findings",
|
|
143
|
+
description: "The findings of one Application, with the lifecycle context that decides how much each " +
|
|
144
|
+
"one matters: whether the affected place is reachable, whether it is exposed, and what a " +
|
|
145
|
+
"previous triage pass decided. An empty priority means no pass has decided yet, never " +
|
|
146
|
+
"that the finding is unimportant.",
|
|
147
|
+
input: {
|
|
148
|
+
application: z.string().describe("The Application's key. Required: the read is scoped to one Application."),
|
|
149
|
+
statuses: z.array(z.string()).optional().describe('Lifecycle statuses to keep: "open", "fixing", "fixed", "verified". Omit for every status.'),
|
|
150
|
+
limit: z.number().int().min(1).optional().describe("Bound on one read. The server caps it either way."),
|
|
151
|
+
},
|
|
152
|
+
annotations: { readOnlyHint: true },
|
|
153
|
+
handler: async (args) => json(await (ctx.gibson.knowledge ?? knowledge).applicationFindings({
|
|
154
|
+
application: args.application,
|
|
155
|
+
...(args.statuses ? { statuses: args.statuses } : {}),
|
|
156
|
+
...(args.limit ? { limit: args.limit } : {}),
|
|
157
|
+
})),
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../registry.js";
|
|
2
|
+
import type { HelperContext } from "./context.js";
|
|
3
|
+
export interface ToolOutcome {
|
|
4
|
+
output: unknown;
|
|
5
|
+
error?: {
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
retryable: boolean;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function formatToolResult(name: string, result: ToolOutcome): {
|
|
12
|
+
[x: string]: unknown;
|
|
13
|
+
content: ({
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
annotations?: {
|
|
17
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
18
|
+
priority?: number | undefined;
|
|
19
|
+
lastModified?: string | undefined;
|
|
20
|
+
} | undefined;
|
|
21
|
+
_meta?: {
|
|
22
|
+
[x: string]: unknown;
|
|
23
|
+
} | undefined;
|
|
24
|
+
} | {
|
|
25
|
+
type: "image";
|
|
26
|
+
data: string;
|
|
27
|
+
mimeType: string;
|
|
28
|
+
annotations?: {
|
|
29
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
30
|
+
priority?: number | undefined;
|
|
31
|
+
lastModified?: string | undefined;
|
|
32
|
+
} | undefined;
|
|
33
|
+
_meta?: {
|
|
34
|
+
[x: string]: unknown;
|
|
35
|
+
} | undefined;
|
|
36
|
+
} | {
|
|
37
|
+
type: "audio";
|
|
38
|
+
data: string;
|
|
39
|
+
mimeType: string;
|
|
40
|
+
annotations?: {
|
|
41
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
42
|
+
priority?: number | undefined;
|
|
43
|
+
lastModified?: string | undefined;
|
|
44
|
+
} | undefined;
|
|
45
|
+
_meta?: {
|
|
46
|
+
[x: string]: unknown;
|
|
47
|
+
} | undefined;
|
|
48
|
+
} | {
|
|
49
|
+
uri: string;
|
|
50
|
+
name: string;
|
|
51
|
+
type: "resource_link";
|
|
52
|
+
description?: string | undefined;
|
|
53
|
+
mimeType?: string | undefined;
|
|
54
|
+
size?: number | undefined;
|
|
55
|
+
annotations?: {
|
|
56
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
57
|
+
priority?: number | undefined;
|
|
58
|
+
lastModified?: string | undefined;
|
|
59
|
+
} | undefined;
|
|
60
|
+
_meta?: {
|
|
61
|
+
[x: string]: unknown;
|
|
62
|
+
} | undefined;
|
|
63
|
+
icons?: {
|
|
64
|
+
src: string;
|
|
65
|
+
mimeType?: string | undefined;
|
|
66
|
+
sizes?: string[] | undefined;
|
|
67
|
+
theme?: "light" | "dark" | undefined;
|
|
68
|
+
}[] | undefined;
|
|
69
|
+
title?: string | undefined;
|
|
70
|
+
} | {
|
|
71
|
+
type: "resource";
|
|
72
|
+
resource: {
|
|
73
|
+
uri: string;
|
|
74
|
+
text: string;
|
|
75
|
+
mimeType?: string | undefined;
|
|
76
|
+
_meta?: {
|
|
77
|
+
[x: string]: unknown;
|
|
78
|
+
} | undefined;
|
|
79
|
+
} | {
|
|
80
|
+
uri: string;
|
|
81
|
+
blob: string;
|
|
82
|
+
mimeType?: string | undefined;
|
|
83
|
+
_meta?: {
|
|
84
|
+
[x: string]: unknown;
|
|
85
|
+
} | undefined;
|
|
86
|
+
};
|
|
87
|
+
annotations?: {
|
|
88
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
89
|
+
priority?: number | undefined;
|
|
90
|
+
lastModified?: string | undefined;
|
|
91
|
+
} | undefined;
|
|
92
|
+
_meta?: {
|
|
93
|
+
[x: string]: unknown;
|
|
94
|
+
} | undefined;
|
|
95
|
+
})[];
|
|
96
|
+
_meta?: {
|
|
97
|
+
[x: string]: unknown;
|
|
98
|
+
progressToken?: string | number | undefined;
|
|
99
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
100
|
+
taskId: string;
|
|
101
|
+
} | undefined;
|
|
102
|
+
} | undefined;
|
|
103
|
+
structuredContent?: {
|
|
104
|
+
[x: string]: unknown;
|
|
105
|
+
} | undefined;
|
|
106
|
+
isError?: boolean | undefined;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The generic tool and plugin surface. `gibson_call_tool` stays even where
|
|
110
|
+
* discovery works: discovery answers `Unimplemented` on some daemons
|
|
111
|
+
* (gibson#1186), and an operator may know a tool by name.
|
|
112
|
+
*/
|
|
113
|
+
export declare function platformToolHelpers(ctx: HelperContext): ToolDefinition[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { callGibsonTool, listGibsonPlugins, listGibsonTools, parseMaybeJSON, queryGibsonPlugin } from "@zeroroot-ai/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineTool } from "../tool.js";
|
|
4
|
+
import { failure, text } from "../tools/result.js";
|
|
5
|
+
export function formatToolResult(name, result) {
|
|
6
|
+
if (result.error) {
|
|
7
|
+
return failure(`${name} failed`, `Gibson tool ${name} failed [${result.error.code}]: ${result.error.message}${result.error.retryable ? " (retryable)" : ""}`);
|
|
8
|
+
}
|
|
9
|
+
const body = typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2);
|
|
10
|
+
return text(name, body ?? "(no output)");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The generic tool and plugin surface. `gibson_call_tool` stays even where
|
|
14
|
+
* discovery works: discovery answers `Unimplemented` on some daemons
|
|
15
|
+
* (gibson#1186), and an operator may know a tool by name.
|
|
16
|
+
*/
|
|
17
|
+
export function platformToolHelpers(ctx) {
|
|
18
|
+
if (!ctx.gibson.session)
|
|
19
|
+
return [];
|
|
20
|
+
const component = ctx.gibson.session.clients.component;
|
|
21
|
+
return [
|
|
22
|
+
defineTool({
|
|
23
|
+
name: "gibson_call_tool",
|
|
24
|
+
description: "Invoke a Gibson tool by name through the harness. Use this when you know a tool's name " +
|
|
25
|
+
"but it is not in your tool list. Gibson tool discovery is not available on every daemon.",
|
|
26
|
+
input: {
|
|
27
|
+
name: z.string().describe("Registered Gibson tool name."),
|
|
28
|
+
input: z.record(z.string(), z.unknown()).describe("Input object matching the tool's input schema."),
|
|
29
|
+
timeout_ms: z.number().int().min(1).optional().describe("Per-call timeout in milliseconds."),
|
|
30
|
+
},
|
|
31
|
+
handler: async (args) => formatToolResult(args.name, await callGibsonTool(component, { name: args.name, input: args.input, ...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}) })),
|
|
32
|
+
}),
|
|
33
|
+
defineTool({
|
|
34
|
+
name: "list_gibson_tools",
|
|
35
|
+
description: "List the Gibson tools available to this tenant, with the input message type each one expects.",
|
|
36
|
+
input: {},
|
|
37
|
+
annotations: { readOnlyHint: true },
|
|
38
|
+
handler: async () => {
|
|
39
|
+
const discovery = await listGibsonTools(component);
|
|
40
|
+
if (discovery.unavailable)
|
|
41
|
+
return text("discovery unavailable", `Tool discovery is unavailable: ${discovery.unavailable}.`);
|
|
42
|
+
if (discovery.tools.length === 0)
|
|
43
|
+
return text("no tools", "No Gibson tools are registered for this tenant.");
|
|
44
|
+
const lines = discovery.tools.map((t) => `- ${t.name} (${t.version}): ${t.description || "no description"}${t.inputMessageType ? `\n input: ${t.inputMessageType}` : ""}`);
|
|
45
|
+
return text(`${discovery.tools.length} tool${discovery.tools.length === 1 ? "" : "s"}`, lines.join("\n"));
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
defineTool({
|
|
49
|
+
name: "list_gibson_plugins",
|
|
50
|
+
description: "List the Gibson plugins in the catalog, annotated with this tenant's enablement and health.",
|
|
51
|
+
input: {},
|
|
52
|
+
annotations: { readOnlyHint: true },
|
|
53
|
+
handler: async () => {
|
|
54
|
+
const plugins = await listGibsonPlugins(component);
|
|
55
|
+
if (plugins.length === 0)
|
|
56
|
+
return text("no plugins", "The catalog holds no plugins for this tenant.");
|
|
57
|
+
const lines = plugins.map((p) => `- ${p.name} (${p.version}): ${p.description || "no description"} [${p.enabled ? "enabled" : "disabled"}, ${p.healthStatus || "health unknown"}]`);
|
|
58
|
+
return text(`${plugins.length} plugin${plugins.length === 1 ? "" : "s"}`, lines.join("\n"));
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
defineTool({
|
|
62
|
+
name: "query_plugin",
|
|
63
|
+
description: "Call a method on a tenant Gibson plugin (a connector or an integration) through the harness.",
|
|
64
|
+
input: {
|
|
65
|
+
plugin: z.string().describe("Plugin name."),
|
|
66
|
+
method: z.string().describe("Method name."),
|
|
67
|
+
params: z.record(z.string(), z.unknown()).optional().describe("Method parameters."),
|
|
68
|
+
},
|
|
69
|
+
handler: async (args) => formatToolResult(`${args.plugin}.${args.method}`, await queryGibsonPlugin(component, { plugin: args.plugin, method: args.method, params: args.params ?? {} })),
|
|
70
|
+
}),
|
|
71
|
+
defineTool({
|
|
72
|
+
name: "parse_gibson_output",
|
|
73
|
+
description: "Decode a Gibson `*_json` string field. A tool is free to return a bare string, so a value " +
|
|
74
|
+
"that does not parse comes back as the raw text rather than an error.",
|
|
75
|
+
input: { raw: z.string().describe("The raw string field.") },
|
|
76
|
+
annotations: { readOnlyHint: true },
|
|
77
|
+
handler: async (args) => text("", JSON.stringify(parseMaybeJSON(args.raw) ?? null, null, 2)),
|
|
78
|
+
}),
|
|
79
|
+
];
|
|
80
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import type { Listen } from "./flags.js";
|
|
4
|
+
import { type Log } from "./log.js";
|
|
5
|
+
/**
|
|
6
|
+
* The streamable-HTTP transport on localhost: the sandbox path. One process
|
|
7
|
+
* serves for the life of the sandbox; each MCP client session (one per Claude
|
|
8
|
+
* Code process) gets its own protocol server over the shared tool registry.
|
|
9
|
+
*
|
|
10
|
+
* POST /mcp a new session starts with `initialize`; later requests
|
|
11
|
+
* carry `mcp-session-id`
|
|
12
|
+
* GET /mcp the session's notification stream
|
|
13
|
+
* DELETE /mcp ends a session
|
|
14
|
+
* GET /healthz liveness and the current posture, as JSON
|
|
15
|
+
*
|
|
16
|
+
* DNS-rebinding protection is on: only the loopback host names are accepted.
|
|
17
|
+
*/
|
|
18
|
+
export interface HttpSurface {
|
|
19
|
+
attach(transport: Transport): Promise<unknown>;
|
|
20
|
+
/** Small JSON for /healthz. */
|
|
21
|
+
health(): Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* The per-turn grant control endpoint (gibson#1706, decision 14). The
|
|
24
|
+
* driver calls it before it feeds Claude a message, and every tool call
|
|
25
|
+
* that follows runs under that dispatch's grant. Absent outside a member
|
|
26
|
+
* sandbox, and then `/turn` answers 404 like any other unknown route.
|
|
27
|
+
*/
|
|
28
|
+
turn?: TurnRoute;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `POST /turn {job_id, grant, callback_endpoint?, insecure?}` puts a turn in
|
|
32
|
+
* force. `DELETE /turn` ends it, and later calls fall back to the base
|
|
33
|
+
* grant. `GET /turn` reports the turn in force.
|
|
34
|
+
*/
|
|
35
|
+
export interface TurnRoute {
|
|
36
|
+
set(turn: {
|
|
37
|
+
jobId: string;
|
|
38
|
+
grant: string;
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
insecure?: boolean;
|
|
41
|
+
}): void;
|
|
42
|
+
clear(): void;
|
|
43
|
+
current(): {
|
|
44
|
+
jobId: string;
|
|
45
|
+
endpoint: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
}
|
|
48
|
+
export interface HttpHandle {
|
|
49
|
+
url: string;
|
|
50
|
+
host: string;
|
|
51
|
+
port: number;
|
|
52
|
+
sessions(): number;
|
|
53
|
+
close(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export declare function readJsonBody(req: IncomingMessage): Promise<unknown>;
|
|
56
|
+
export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
|
|
57
|
+
export declare function serveHttp(surface: HttpSurface, listen: Listen, log: Log): Promise<HttpHandle>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { TAG } from "./log.js";
|
|
6
|
+
const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
export async function readJsonBody(req) {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
let size = 0;
|
|
10
|
+
for await (const c of req) {
|
|
11
|
+
const buf = c;
|
|
12
|
+
size += buf.byteLength;
|
|
13
|
+
if (size > MAX_BODY_BYTES)
|
|
14
|
+
throw new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`);
|
|
15
|
+
chunks.push(buf);
|
|
16
|
+
}
|
|
17
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
18
|
+
if (!raw)
|
|
19
|
+
return undefined;
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
export function sendJson(res, status, body) {
|
|
23
|
+
const text = JSON.stringify(body);
|
|
24
|
+
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
25
|
+
res.end(text);
|
|
26
|
+
}
|
|
27
|
+
export async function serveHttp(surface, listen, log) {
|
|
28
|
+
const sessions = new Map();
|
|
29
|
+
let allowedHosts = [];
|
|
30
|
+
const handleMcp = async (req, res) => {
|
|
31
|
+
const header = req.headers["mcp-session-id"];
|
|
32
|
+
const sid = Array.isArray(header) ? header[0] : header;
|
|
33
|
+
if (sid) {
|
|
34
|
+
const transport = sessions.get(sid);
|
|
35
|
+
if (!transport) {
|
|
36
|
+
sendJson(res, 404, { error: "unknown MCP session; start a new one with initialize" });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await transport.handleRequest(req, res);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (req.method !== "POST") {
|
|
43
|
+
sendJson(res, 400, { error: "a new MCP session starts with a POST initialize request" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const body = await readJsonBody(req);
|
|
47
|
+
if (!isInitializeRequest(body)) {
|
|
48
|
+
sendJson(res, 400, { error: "a new MCP session starts with initialize; later requests carry mcp-session-id" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const transport = new StreamableHTTPServerTransport({
|
|
52
|
+
sessionIdGenerator: () => randomUUID(),
|
|
53
|
+
onsessioninitialized: (id) => {
|
|
54
|
+
sessions.set(id, transport);
|
|
55
|
+
log(`${TAG} http: session ${id} opened (${sessions.size} open)`);
|
|
56
|
+
},
|
|
57
|
+
onsessionclosed: (id) => {
|
|
58
|
+
sessions.delete(id);
|
|
59
|
+
log(`${TAG} http: session ${id} closed (${sessions.size} open)`);
|
|
60
|
+
},
|
|
61
|
+
enableDnsRebindingProtection: true,
|
|
62
|
+
allowedHosts,
|
|
63
|
+
});
|
|
64
|
+
transport.onclose = () => {
|
|
65
|
+
if (transport.sessionId)
|
|
66
|
+
sessions.delete(transport.sessionId);
|
|
67
|
+
};
|
|
68
|
+
await surface.attach(transport);
|
|
69
|
+
await transport.handleRequest(req, res, body);
|
|
70
|
+
};
|
|
71
|
+
const handleTurn = async (turn, req, res) => {
|
|
72
|
+
if (req.method === "GET") {
|
|
73
|
+
const current = turn.current();
|
|
74
|
+
return sendJson(res, 200, current ? { job_id: current.jobId, endpoint: current.endpoint } : { job_id: null });
|
|
75
|
+
}
|
|
76
|
+
if (req.method === "DELETE") {
|
|
77
|
+
turn.clear();
|
|
78
|
+
return sendJson(res, 200, { job_id: null });
|
|
79
|
+
}
|
|
80
|
+
if (req.method !== "POST")
|
|
81
|
+
return sendJson(res, 405, { error: "POST to set a turn, DELETE to end it, GET to read it" });
|
|
82
|
+
const body = (await readJsonBody(req));
|
|
83
|
+
const jobId = typeof body?.job_id === "string" ? body.job_id : "";
|
|
84
|
+
const grant = typeof body?.grant === "string" ? body.grant : "";
|
|
85
|
+
if (!jobId || !grant) {
|
|
86
|
+
return sendJson(res, 400, { error: "job_id and grant are both required: a turn is one dispatch's grant, and a grant with no job attributes the work to nothing" });
|
|
87
|
+
}
|
|
88
|
+
const endpoint = typeof body?.callback_endpoint === "string" ? body.callback_endpoint : undefined;
|
|
89
|
+
turn.set({ jobId, grant, ...(endpoint ? { endpoint } : {}), ...(typeof body?.insecure === "boolean" ? { insecure: body.insecure } : {}) });
|
|
90
|
+
const current = turn.current();
|
|
91
|
+
return sendJson(res, 200, { job_id: jobId, endpoint: current?.endpoint ?? endpoint ?? "" });
|
|
92
|
+
};
|
|
93
|
+
const server = createServer((req, res) => {
|
|
94
|
+
void (async () => {
|
|
95
|
+
const url = new URL(req.url ?? "/", `http://${listen.host}:${listen.port || 0}`);
|
|
96
|
+
try {
|
|
97
|
+
if (url.pathname === "/turn" && surface.turn)
|
|
98
|
+
return await handleTurn(surface.turn, req, res);
|
|
99
|
+
if (url.pathname === "/mcp")
|
|
100
|
+
return await handleMcp(req, res);
|
|
101
|
+
if (url.pathname === "/healthz" && req.method === "GET")
|
|
102
|
+
return sendJson(res, 200, { ok: true, sessions: sessions.size, ...surface.health() });
|
|
103
|
+
sendJson(res, 404, { error: `no route ${req.method} ${url.pathname}` });
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
log(`${TAG} http: ${req.method} ${url.pathname}: ${e.message}`);
|
|
107
|
+
if (!res.headersSent)
|
|
108
|
+
sendJson(res, 500, { error: e.message });
|
|
109
|
+
else
|
|
110
|
+
res.end();
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
});
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
server.once("error", reject);
|
|
116
|
+
server.listen(listen.port, listen.host, () => {
|
|
117
|
+
server.off("error", reject);
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
const addr = server.address();
|
|
122
|
+
const host = addr.family === "IPv6" ? `[${addr.address}]` : addr.address;
|
|
123
|
+
allowedHosts = [`${host}:${addr.port}`, `localhost:${addr.port}`, `127.0.0.1:${addr.port}`, `[::1]:${addr.port}`, host, "localhost", "127.0.0.1", "[::1]"];
|
|
124
|
+
return {
|
|
125
|
+
url: `http://${host}:${addr.port}/mcp`,
|
|
126
|
+
host: addr.address,
|
|
127
|
+
port: addr.port,
|
|
128
|
+
sessions: () => sessions.size,
|
|
129
|
+
close: async () => {
|
|
130
|
+
for (const t of sessions.values())
|
|
131
|
+
await t.close().catch(() => { });
|
|
132
|
+
sessions.clear();
|
|
133
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
134
|
+
server.closeAllConnections?.();
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|