Files
fusion/plugins/fusion-plugin-grok-runtime/src/mcp-forwarding.ts
gsxdsm 6267a762a4 feat: drive Grok CLI sessions over ACP with tools, skills, and MCP
Replace one-shot grok -p JSON with native grok agent stdio (ACP) for realtime
streaming, tool visibility, and multi-turn sessions. Vendor the ACP client
into fusion-plugin-grok-runtime, forward Fusion fn_* tools and operator MCP,
stage Fusion skills via --plugin-dir, authenticate per xAI headless docs, and
align project chat manager store resolution so Grok chat sessions can send.
2026-07-11 23:23:31 -07:00

115 lines
3.9 KiB
TypeScript

/*
FNXC:GrokAcp 2026-07-11-14:00:
Convert engine-resolved MCP server definitions (FN-7022 three-transport shape)
into ACP `session/new.mcpServers` entries so Grok agent stdio receives the same
operator-approved MCP set as other Fusion AI lanes. Env/header secrets are
already materialized by the engine; this module only reshapes them and never logs
server contents.
*/
export type AcpMcpServer =
| {
name: string;
command: string;
args: string[];
env: { name: string; value: string }[];
}
| {
type: "http";
name: string;
url: string;
headers: { name: string; value: string }[];
}
| {
type: "sse";
name: string;
url: string;
headers: { name: string; value: string }[];
};
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function mapEntries(map: Record<string, string> | undefined): { name: string; value: string }[] {
if (!map) return [];
return Object.entries(map)
.filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string")
.map(([name, value]) => ({ name, value }));
}
/**
* Normalize engine `mcpServers` (ResolvedMcpServerDefinition or legacy ACP
* stdio shape) into the ACP wire format Grok accepts.
*/
export function toAcpMcpServers(servers: unknown): AcpMcpServer[] {
if (!Array.isArray(servers) || servers.length === 0) return [];
const out: AcpMcpServer[] = [];
for (const raw of servers) {
const server = asRecord(raw);
if (!server) continue;
const name = typeof server.name === "string" ? server.name.trim() : "";
if (!name || server.enabled === false) continue;
// Legacy ACP stdio shape: { name, command, args, env: [{name,value}] }
if (typeof server.command === "string" && server.command.trim() && !("transport" in server) && !("type" in server) && !("url" in server)) {
const envPairs = Array.isArray(server.env)
? server.env
.map((entry) => asRecord(entry))
.filter((entry): entry is Record<string, unknown> => Boolean(entry))
.filter((entry) => typeof entry.name === "string" && typeof entry.value === "string")
.map((entry) => ({ name: String(entry.name), value: String(entry.value) }))
: mapEntries(asRecord(server.env) as Record<string, string> | undefined);
out.push({
name,
command: server.command.trim(),
args: Array.isArray(server.args) ? server.args.filter((a): a is string => typeof a === "string") : [],
env: envPairs,
});
continue;
}
const transport = typeof server.transport === "string" ? server.transport : typeof server.type === "string" ? server.type : "stdio";
if (transport === "stdio") {
const command = typeof server.command === "string" ? server.command.trim() : "";
if (!command) continue;
out.push({
name,
command,
args: Array.isArray(server.args) ? server.args.filter((a): a is string => typeof a === "string") : [],
env: mapEntries(asRecord(server.env) as Record<string, string> | undefined),
});
continue;
}
if (transport === "http" || transport === "streamable-http") {
const url = typeof server.url === "string" ? server.url.trim() : "";
if (!url) continue;
out.push({
type: "http",
name,
url,
headers: mapEntries(asRecord(server.headers) as Record<string, string> | undefined),
});
continue;
}
if (transport === "sse") {
const url = typeof server.url === "string" ? server.url.trim() : "";
if (!url) continue;
out.push({
type: "sse",
name,
url,
headers: mapEntries(asRecord(server.headers) as Record<string, string> | undefined),
});
}
}
return out;
}