Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
93 lines
5.4 KiB
TypeScript
93 lines
5.4 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const mcpProtocols = ["legacy", "auto", "2026-07-28"] as const;
|
|
export const mcpTimeoutDefaults = { startup: 10_000, catalog: 10_000, execution: 60_000 };
|
|
const timeout = z.object({
|
|
startup: z.number().int().positive().optional(),
|
|
catalog: z.number().int().positive().optional(),
|
|
execution: z.number().int().positive().optional(),
|
|
}).strict();
|
|
const common = {
|
|
disabled: z.boolean().optional(), codemode: z.boolean().optional(),
|
|
protocol: z.enum(mcpProtocols).optional(), timeout: timeout.optional(),
|
|
};
|
|
const oauth = z.object({
|
|
client_id: z.string().optional(), client_secret: z.string().optional(), scope: z.string().optional(),
|
|
callback_port: z.number().int().min(1).max(65535).optional(), redirect_uri: z.string().optional(),
|
|
auth_server_metadata_url: z.string().optional(),
|
|
}).strict();
|
|
const server = z.discriminatedUnion("type", [
|
|
z.object({ ...common, type: z.literal("local"), command: z.array(z.string()).min(1), cwd: z.string().optional(), environment: z.record(z.string()).optional() }).strict(),
|
|
z.object({ ...common, type: z.literal("remote"), url: z.string().min(1), headers: z.record(z.string()).optional(), oauth: z.union([oauth, z.literal(false)]).optional() }).strict(),
|
|
]);
|
|
export type McpServerConfig = z.infer<typeof server>;
|
|
export const isMcpTool = (name: string) => name.startsWith("mcp_") ||
|
|
name === "opencode_list_mcp_resources" || name === "opencode_read_mcp_resource";
|
|
|
|
export function normalizeMcpServer(name: string, input: unknown): McpServerConfig {
|
|
if (!/^mcp_[A-Za-z0-9_-]+$/.test(name)) throw new Error("MCP server names must start with mcp_ and contain only letters, digits, _ or -");
|
|
const parsed = server.safeParse(input);
|
|
if (!parsed.success) throw new Error(`invalid MCP configuration for ${name}`);
|
|
const value = parsed.data;
|
|
if (value.type === "local" && !value.command[0]?.trim()) throw new Error(`invalid MCP command for ${name}`);
|
|
if (value.type === "remote" && !/\{env:[A-Za-z_][A-Za-z0-9_]*\}/.test(value.url)) {
|
|
try {
|
|
const url = new URL(value.url);
|
|
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error();
|
|
} catch { throw new Error(`invalid MCP endpoint for ${name}`); }
|
|
}
|
|
return { ...value, protocol: value.protocol ?? (value.type === "remote" ? "auto" : "legacy") };
|
|
}
|
|
|
|
export function normalizeMcpConfig(input: unknown) {
|
|
const parsed = z.object({ timeout: timeout.optional(), servers: z.record(z.unknown()).default({}) }).strict().safeParse(input ?? {});
|
|
if (!parsed.success) throw new Error("invalid MCP configuration");
|
|
return {
|
|
timeout: { ...mcpTimeoutDefaults, ...parsed.data.timeout },
|
|
servers: Object.fromEntries(Object.entries(parsed.data.servers).map(([name, value]) => [name, normalizeMcpServer(name, value)])),
|
|
};
|
|
}
|
|
|
|
type Rule = { action: string; resource: string; effect: "allow" | "deny" | "ask" };
|
|
const matches = (pattern: string, value: string) => new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*").replaceAll("?", ".")}$`).test(value);
|
|
|
|
// Only exact, explicit service rules are an administrator's read-only grant.
|
|
// Built-in defaults, broad allows and saved user approvals are not grants.
|
|
export function mcpReadonlyTools(input: Record<string, unknown>) {
|
|
const agents = input.agents as { instruction?: { permissions?: Rule[] } } | undefined;
|
|
const rules = [...((input.permissions ?? []) as Rule[]), ...(agents?.instruction?.permissions ?? [])];
|
|
const candidates = rules.filter((rule) => rule.effect === "allow" && /^mcp_[A-Za-z0-9_.-]+$/.test(rule.action)).map((rule) => rule.action);
|
|
return [...new Set(candidates)].filter((name) => {
|
|
let effect: Rule["effect"] = "ask";
|
|
for (const rule of rules) if (matches(rule.action, name) && matches(rule.resource, "*")) effect = rule.effect;
|
|
return effect === "allow";
|
|
}).sort();
|
|
}
|
|
|
|
const baseEnvironment = new Set(["PATH", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR", "SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT"]);
|
|
const reservedEnvironment = /^(?:AGENT_INTERNAL_TOKEN|TJWATER_AGENT_.*|OPENCODE_.*|ORCA_.*|NODE_OPTIONS|BUN_OPTIONS)$/i;
|
|
export function mcpLocalEnvironment(parent: NodeJS.ProcessEnv, explicit: Record<string, string> = {}) {
|
|
// Native stdio inherits its host environment. Override non-runtime entries
|
|
// with empty values before adding credentials intentionally owned by this MCP.
|
|
const environment = Object.fromEntries(Object.entries(parent).map(([key, value]) => [key, baseEnvironment.has(key.toUpperCase()) ? value ?? "" : ""]));
|
|
Object.assign(environment, explicit);
|
|
for (const key of Object.keys(environment)) if (reservedEnvironment.test(key)) environment[key] = "";
|
|
return environment;
|
|
}
|
|
|
|
export const mcpSessionPolicySchema = z.object({
|
|
mode: z.enum(["interactive", "readonly", "disabled"]),
|
|
readonly_tools: z.array(z.string()),
|
|
query_tools: z.array(z.string()).default([]),
|
|
}).strict();
|
|
export type McpSessionPolicy = z.infer<typeof mcpSessionPolicySchema>;
|
|
export const mcpServerStatusSchema = z.object({
|
|
name: z.string(), type: z.enum(["local", "remote"]),
|
|
status: z.enum(["connected", "pending", "disabled", "failed", "needs_auth"]),
|
|
configured_protocol: z.enum(mcpProtocols), tools: z.array(z.string()),
|
|
}).strict();
|
|
export const mcpRuntimeReportSchema = z.object({
|
|
servers: z.array(mcpServerStatusSchema), configuration_errors: z.array(z.string()),
|
|
}).strict();
|
|
export type McpRuntimeReport = z.infer<typeof mcpRuntimeReportSchema>;
|