@yagni-app/code-staging 0.1.0-staging.1002.1 → 0.1.0-staging.1009.1
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
3
|
+
import { fetchMcpServers as defaultFetchMcpServers } from "./mcpTools.js";
|
|
3
4
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
4
5
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
5
6
|
import { type ContextBrief, type ModelEntry } from "./config.js";
|
|
@@ -35,6 +36,12 @@ export interface RegisterYagniDeps {
|
|
|
35
36
|
getToken: () => string | undefined;
|
|
36
37
|
fetchImpl?: typeof fetch;
|
|
37
38
|
}) => Promise<ContextBrief | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Workspace MCP server list seam (YAG-446). The default hits
|
|
41
|
+
* `/api/yagni-code/mcp/servers` and is fail-soft: older backends and
|
|
42
|
+
* un-rescoped tokens yield `[]`, never a startup error.
|
|
43
|
+
*/
|
|
44
|
+
fetchMcpServers?: typeof defaultFetchMcpServers;
|
|
38
45
|
/**
|
|
39
46
|
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
40
47
|
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
package/dist/extension/index.js
CHANGED
|
@@ -10,9 +10,10 @@ import { BRAND_NAME, brandSystemPrompt, buildMastheadString } from "./branding.j
|
|
|
10
10
|
import { registerCostCommand } from "./costHud.js";
|
|
11
11
|
import { isFreshWorkspace, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
12
12
|
import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
|
|
13
|
+
import { fetchMcpServers as defaultFetchMcpServers, registerMcpCommand, registerMcpTools, } from "./mcpTools.js";
|
|
13
14
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
14
15
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
15
|
-
import { registerPermissionGate } from "./permission.js";
|
|
16
|
+
import { DEFAULT_PERMISSION_POLICY, registerPermissionGate } from "./permission.js";
|
|
16
17
|
import { registerSubagents } from "./subagents.js";
|
|
17
18
|
import { registerTodos } from "./todos.js";
|
|
18
19
|
import { registerDecisionCommands } from "./decisions.js";
|
|
@@ -103,10 +104,36 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
103
104
|
const decisionCapture = evalMode ? undefined : makeDecisionCapture(decisionClientOpts);
|
|
104
105
|
if (!evalMode)
|
|
105
106
|
registerDecisionCommands(pi, decisionClientOpts);
|
|
107
|
+
// Workspace MCP servers (YAG-446): register one tool per enabled server
|
|
108
|
+
// tool, executing through the backend proxy, plus the /mcp listing
|
|
109
|
+
// command. Fail-soft fetch (older backend / un-rescoped token → no MCP
|
|
110
|
+
// tools); skipped in eval mode like the other external-side-effect tools.
|
|
111
|
+
const fetchMcp = deps.fetchMcpServers ?? defaultFetchMcpServers;
|
|
112
|
+
const mcpServers = evalMode
|
|
113
|
+
? []
|
|
114
|
+
: await fetchMcp({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
115
|
+
const { mutatingToolNames: mcpMutatingTools } = registerMcpTools(pi, mcpServers, { baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
116
|
+
registerMcpCommand(pi, mcpServers, { baseUrl });
|
|
106
117
|
// P3 + W4: interactive permission tiers + plan mode via /mode, now with a
|
|
107
118
|
// session bless store (three-way review-mode select) and a capture hook that
|
|
108
119
|
// drafts a decision on "don't ask again". Default auto, so still additive.
|
|
120
|
+
// Mutating MCP tools join write/edit/bash in the gate policy: plan mode
|
|
121
|
+
// holds them, review mode confirms them.
|
|
109
122
|
registerPermissionGate(pi, {
|
|
123
|
+
...(mcpMutatingTools.length > 0
|
|
124
|
+
? {
|
|
125
|
+
policy: {
|
|
126
|
+
planBlockTools: [
|
|
127
|
+
...DEFAULT_PERMISSION_POLICY.planBlockTools,
|
|
128
|
+
...mcpMutatingTools,
|
|
129
|
+
],
|
|
130
|
+
reviewConfirmTools: [
|
|
131
|
+
...DEFAULT_PERMISSION_POLICY.reviewConfirmTools,
|
|
132
|
+
...mcpMutatingTools,
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
110
137
|
onBlessRemember: decisionCapture
|
|
111
138
|
? (ctx, info) => decisionCapture.captureFromBless(ctx, info)
|
|
112
139
|
: undefined,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace MCP servers in YAGNI Code (YAG-446).
|
|
3
|
+
*
|
|
4
|
+
* The workspace's registered MCP servers (Connections → MCP Servers) are
|
|
5
|
+
* the config source: at session start we fetch the list from the backend
|
|
6
|
+
* and register one pi tool per enabled server tool, named with the same
|
|
7
|
+
* `mcp_<slug>__<tool>` wire shape the backend executor parses. Calls
|
|
8
|
+
* execute THROUGH the backend (`POST /api/yagni-code/mcp/call`), so
|
|
9
|
+
* credentials never reach this machine and the backend's egress guard,
|
|
10
|
+
* autonomy gate, per-user credential policy, and audit trail all apply.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is fail-soft against an older backend or an
|
|
13
|
+
* un-rescoped token: a 403/404/network failure on the list fetch means
|
|
14
|
+
* no MCP tools and no startup error.
|
|
15
|
+
*/
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
export interface McpToolInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string | null;
|
|
20
|
+
inputSchema: unknown;
|
|
21
|
+
riskClass: string;
|
|
22
|
+
}
|
|
23
|
+
export interface McpServerInfo {
|
|
24
|
+
slug: string;
|
|
25
|
+
name: string;
|
|
26
|
+
tools: McpToolInfo[];
|
|
27
|
+
viewerCredential: {
|
|
28
|
+
source: "user" | "workspace";
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface McpClientOpts {
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
getToken: () => string | undefined;
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
}
|
|
36
|
+
export declare function mcpWireToolName(slug: string, tool: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch the workspace's MCP servers. Fail-soft by design: any non-OK
|
|
39
|
+
* response (older backend without the endpoints, token minted before the
|
|
40
|
+
* `yagni_code:mcp` scope existed) or transport error returns `[]`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchMcpServers(opts: McpClientOpts): Promise<McpServerInfo[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Register every reachable MCP tool. Returns the wire names of MUTATING
|
|
45
|
+
* tools so the caller can extend the permission-gate policy — plan mode
|
|
46
|
+
* holds them, review mode confirms them, exactly like write/edit/bash.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerMcpTools(pi: ExtensionAPI, servers: McpServerInfo[], opts: McpClientOpts): {
|
|
49
|
+
mutatingToolNames: string[];
|
|
50
|
+
};
|
|
51
|
+
/** Render the /mcp listing (pure, for tests). */
|
|
52
|
+
export declare function renderMcpListing(servers: McpServerInfo[], baseUrl: string): string;
|
|
53
|
+
/** Wire the `/mcp` command: list servers, tools, and credential status. */
|
|
54
|
+
export declare function registerMcpCommand(pi: ExtensionAPI, servers: McpServerInfo[], opts: {
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
}): void;
|
|
57
|
+
//# sourceMappingURL=mcpTools.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
3
|
+
export function mcpWireToolName(slug, tool) {
|
|
4
|
+
return `mcp_${slug}__${tool}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fetch the workspace's MCP servers. Fail-soft by design: any non-OK
|
|
8
|
+
* response (older backend without the endpoints, token minted before the
|
|
9
|
+
* `yagni_code:mcp` scope existed) or transport error returns `[]`.
|
|
10
|
+
*/
|
|
11
|
+
export async function fetchMcpServers(opts) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/mcp/servers`, {
|
|
14
|
+
method: "GET",
|
|
15
|
+
headers: { authorization: `Bearer ${opts.getToken() ?? ""}` },
|
|
16
|
+
}, { fetchImpl: opts.fetchImpl });
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return [];
|
|
19
|
+
const data = (await res.json());
|
|
20
|
+
if (!Array.isArray(data.servers))
|
|
21
|
+
return [];
|
|
22
|
+
return data.servers;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function schemaFor(tool) {
|
|
29
|
+
const schema = tool.inputSchema;
|
|
30
|
+
if (schema !== null &&
|
|
31
|
+
typeof schema === "object" &&
|
|
32
|
+
!Array.isArray(schema) &&
|
|
33
|
+
Object.keys(schema).length > 0) {
|
|
34
|
+
// MCP inputSchemas are plain JSON Schema objects, which is exactly what
|
|
35
|
+
// TypeBox schemas are at runtime — pass the server's schema through so
|
|
36
|
+
// the model sees real parameter shapes.
|
|
37
|
+
return schema;
|
|
38
|
+
}
|
|
39
|
+
return Type.Object({}, { additionalProperties: true });
|
|
40
|
+
}
|
|
41
|
+
function makeMcpTool(server, tool, opts) {
|
|
42
|
+
const wireName = mcpWireToolName(server.slug, tool.name);
|
|
43
|
+
return {
|
|
44
|
+
name: wireName,
|
|
45
|
+
label: `${server.name}: ${tool.name}`,
|
|
46
|
+
description: `${tool.description ?? `MCP tool ${tool.name}`} ` +
|
|
47
|
+
`(MCP server "${server.name}", risk: ${tool.riskClass}; runs through YAGNI)`,
|
|
48
|
+
parameters: schemaFor(tool),
|
|
49
|
+
async execute(_toolCallId, params, signal) {
|
|
50
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/mcp/call`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"content-type": "application/json",
|
|
54
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
slug: server.slug,
|
|
58
|
+
tool: tool.name,
|
|
59
|
+
args: params ?? {},
|
|
60
|
+
}),
|
|
61
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(await friendlyFetchError(wireName, res));
|
|
64
|
+
}
|
|
65
|
+
const result = (await res.json());
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
const suffix = result.errorCode === "user_credential_required"
|
|
68
|
+
? ` Bind your token at ${opts.baseUrl}/services/mcp.`
|
|
69
|
+
: "";
|
|
70
|
+
throw new Error(`${result.error ?? "MCP tool call failed"}${suffix}`);
|
|
71
|
+
}
|
|
72
|
+
const text = typeof result.result === "string"
|
|
73
|
+
? result.result
|
|
74
|
+
: JSON.stringify(result.result ?? null, null, 2);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text }],
|
|
77
|
+
details: { riskClass: tool.riskClass },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Register every reachable MCP tool. Returns the wire names of MUTATING
|
|
84
|
+
* tools so the caller can extend the permission-gate policy — plan mode
|
|
85
|
+
* holds them, review mode confirms them, exactly like write/edit/bash.
|
|
86
|
+
*/
|
|
87
|
+
export function registerMcpTools(pi, servers, opts) {
|
|
88
|
+
const mutatingToolNames = [];
|
|
89
|
+
for (const server of servers) {
|
|
90
|
+
for (const tool of server.tools) {
|
|
91
|
+
pi.registerTool(makeMcpTool(server, tool, opts));
|
|
92
|
+
if (tool.riskClass !== "read_only") {
|
|
93
|
+
mutatingToolNames.push(mcpWireToolName(server.slug, tool.name));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { mutatingToolNames };
|
|
98
|
+
}
|
|
99
|
+
/** Render the /mcp listing (pure, for tests). */
|
|
100
|
+
export function renderMcpListing(servers, baseUrl) {
|
|
101
|
+
if (servers.length === 0) {
|
|
102
|
+
return [
|
|
103
|
+
"No MCP servers connected for this workspace.",
|
|
104
|
+
`An admin can register one under Connections: ${baseUrl}/services/mcp`,
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
const lines = ["Workspace MCP servers:"];
|
|
108
|
+
for (const server of servers) {
|
|
109
|
+
const mutating = server.tools.filter((t) => t.riskClass !== "read_only").length;
|
|
110
|
+
const credential = server.viewerCredential.source === "user"
|
|
111
|
+
? "connected as you"
|
|
112
|
+
: "workspace credential";
|
|
113
|
+
lines.push(` ${server.name} (${server.slug}) — ${server.tools.length} tool${server.tools.length === 1 ? "" : "s"}` +
|
|
114
|
+
`${mutating > 0 ? ` (${mutating} mutating)` : ""} · ${credential}`);
|
|
115
|
+
for (const tool of server.tools) {
|
|
116
|
+
lines.push(` ${mcpWireToolName(server.slug, tool.name)} [${tool.riskClass}]`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
lines.push(`Bind a personal token (writes carry your authority): ${baseUrl}/services/mcp`);
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
/** Wire the `/mcp` command: list servers, tools, and credential status. */
|
|
123
|
+
export function registerMcpCommand(pi, servers, opts) {
|
|
124
|
+
pi.registerCommand("mcp", {
|
|
125
|
+
description: "List this workspace's MCP servers, their tools, and your credential status.",
|
|
126
|
+
handler: async (_args, ctx) => {
|
|
127
|
+
if (ctx.hasUI)
|
|
128
|
+
ctx.ui.notify(renderMcpListing(servers, opts.baseUrl), "info");
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=mcpTools.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.1.0-staging.
|
|
3
|
+
"version": "0.1.0-staging.1009.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "0.83.0",
|
|
39
39
|
"typebox": "^1.1.38"
|
|
40
40
|
},
|
|
41
|
-
"yagniSourceSha": "
|
|
41
|
+
"yagniSourceSha": "fb42dd685b7ca9834b2861f48493f4ed6fd1caaa"
|
|
42
42
|
}
|