From da7200a394971ee3d80b6d360ab318546baee4a0 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Wed, 23 Sep 2026 13:15:39 +0200 Subject: [PATCH] Improve MCP guidance and tool availability --- coding-agent-mcp/src/coding-agent-index.js | 17 +++++++++-------- dev-server-mcp/services.example.json | 15 +++++++++++++++ dev-server-mcp/services.localhost.json | 1 + dev-server-mcp/src/config.js | 9 +++++++++ dev-server-mcp/src/dev-server-service.js | 5 ++++- dev-server-mcp/src/tool-definitions.js | 2 +- dev-server-mcp/test/dev-server-service.test.js | 14 ++++++++++++++ 7 files changed, 53 insertions(+), 10 deletions(-) diff --git a/coding-agent-mcp/src/coding-agent-index.js b/coding-agent-mcp/src/coding-agent-index.js index 57e2781..bfabfbb 100644 --- a/coding-agent-mcp/src/coding-agent-index.js +++ b/coding-agent-mcp/src/coding-agent-index.js @@ -12,11 +12,6 @@ import { AuditLog } from "./audit-log.js"; const SERVER_NAME = "coding-agent-mcp"; const SERVER_VERSION = "1.0.0"; -const COMBINED_TOOL_DEFINITIONS = [ - ...TOOL_DEFINITIONS, - ...COMMAND_TOOL_DEFINITIONS -]; - function parseApiKeys() { const rawValue = process.env.CODING_AGENT_MCP_API_KEYS ?? process.env.MCP_API_KEYS ?? ""; return rawValue @@ -45,10 +40,16 @@ async function main() { }); const workspaceService = new WorkspaceService({ roots: options.roots }); const auditLog = new AuditLog(process.env.CODING_AGENT_MCP_AUDIT_LOG); + const executionBackend = resolveExecutionBackend(options.transport); const commandService = new CommandService({ roots: options.roots, - executionBackend: resolveExecutionBackend(options.transport) + executionBackend }); + // Do not advertise a command tool that this deployment intentionally cannot execute. An agent + // cannot recover from that failure, and seeing it in tools/list makes it waste a turn trying. + const tools = executionBackend === "local" + ? [...TOOL_DEFINITIONS, ...COMMAND_TOOL_DEFINITIONS] + : TOOL_DEFINITIONS; const callTool = async (toolName, toolArguments, context) => { switch (toolName) { @@ -94,7 +95,7 @@ async function main() { startToolServer({ serverName: SERVER_NAME, serverVersion: SERVER_VERSION, - tools: COMBINED_TOOL_DEFINITIONS, + tools, callTool, auditLog }); @@ -104,7 +105,7 @@ async function main() { const listener = await startHttpToolServer({ serverName: SERVER_NAME, serverVersion: SERVER_VERSION, - tools: COMBINED_TOOL_DEFINITIONS, + tools, callTool, onSessionInitialize: (context) => workspaceService.ensureWorkspace(context), auditLog, diff --git a/dev-server-mcp/services.example.json b/dev-server-mcp/services.example.json index c8151f0..cfc47ed 100644 --- a/dev-server-mcp/services.example.json +++ b/dev-server-mcp/services.example.json @@ -71,6 +71,7 @@ ], "cwd": ".", "workspaceMode": "execution", + "agentInstructions": "This service runs npm ci and then npm run dev. Before starting, create a valid package.json with a dev script and the matching package-lock.json. The dev server must honour PORT and bind to 0.0.0.0. Navigate only to the publicUrl returned by a successful start_service.", "install": { "command": "npm", "args": [ @@ -89,6 +90,20 @@ "PORT": "${port}" } }, + "execution-node": { + "command": "node", + "args": ["server.js", "${port}"], + "cwd": ".", + "workspaceMode": "execution", + "agentInstructions": "Use this for a dependency-free page in an empty workspace. Create server.js in the workspace root; it must serve the page, listen on 0.0.0.0, and use process.env.PORT. No package.json or lockfile is required. Navigate only to the publicUrl returned by a successful start_service.", + "publicUrl": "http://dev-server-worker:${port}", + "healthUrl": "http://127.0.0.1:${port}/", + "startupTimeoutMs": 20000, + "shutdownTimeoutMs": 5000, + "env": { + "PORT": "${port}" + } + }, "execution-api": { "command": "./mvnw", "args": [ diff --git a/dev-server-mcp/services.localhost.json b/dev-server-mcp/services.localhost.json index 70b2404..23e0844 100644 --- a/dev-server-mcp/services.localhost.json +++ b/dev-server-mcp/services.localhost.json @@ -16,6 +16,7 @@ "args": ["server.js", "${port}"], "cwd": ".", "workspaceMode": "execution", + "agentInstructions": "For an empty workspace, create server.js in the workspace root. It is started with Node and must serve the page on host 0.0.0.0 using process.env.PORT. After a successful start, navigate only to the publicUrl returned by start_service.", "publicUrl": "http://dev-server-worker:${port}", "healthUrl": "http://127.0.0.1:${port}/", "startupTimeoutMs": 20000, diff --git a/dev-server-mcp/src/config.js b/dev-server-mcp/src/config.js index a016c6a..0829880 100644 --- a/dev-server-mcp/src/config.js +++ b/dev-server-mcp/src/config.js @@ -34,6 +34,14 @@ function boundedNumber(value, fallback, minimum, maximum, label) { return number; } +function agentInstructions(value, id) { + if (value === undefined || value === null) return null; + if (typeof value !== "string" || value.trim() === "" || value.length > 4096 || value.includes("\0")) { + throw new Error(`Invalid agentInstructions for service: ${id}`); + } + return value.trim(); +} + function relativePath(value, label) { if (typeof value !== "string" || path.isAbsolute(value) || value.includes("\0")) throw new Error(`Invalid ${label}`); const normalized = path.normalize(value); @@ -116,6 +124,7 @@ export function loadDevServerConfig({ configPath, workspaceRoot, allowedCommands needsPort, install: installCommand(value.install, commandAllowlist, id), env: environment, + agentInstructions: agentInstructions(value.agentInstructions, id), publicUrl, healthUrl, startupTimeoutMs: boundedNumber(value.startupTimeoutMs, 15000, 100, 600000, `${id}.startupTimeoutMs`), diff --git a/dev-server-mcp/src/dev-server-service.js b/dev-server-mcp/src/dev-server-service.js index 83e9bf0..62b1b9b 100644 --- a/dev-server-mcp/src/dev-server-service.js +++ b/dev-server-mcp/src/dev-server-service.js @@ -67,7 +67,10 @@ function publicStatus(definition, instance) { exitCode: instance?.exitCode ?? null, signal: instance?.signal ?? null, cwd: instance?.clientCwd ?? definition.clientCwd, - publicUrl: isUnresolved(resolvedUrl) ? null : (resolvedUrl ?? null) + publicUrl: isUnresolved(resolvedUrl) ? null : (resolvedUrl ?? null), + // Operator-written, non-secret instructions for an agent creating a project from scratch. + // The commands themselves stay operator-owned; the agent receives the contract it must meet. + ...(definition.agentInstructions ? { agentInstructions: definition.agentInstructions } : {}) }; } diff --git a/dev-server-mcp/src/tool-definitions.js b/dev-server-mcp/src/tool-definitions.js index 5c6440b..778bdba 100644 --- a/dev-server-mcp/src/tool-definitions.js +++ b/dev-server-mcp/src/tool-definitions.js @@ -1,7 +1,7 @@ const serviceProperty = { type: "string", pattern: "^[a-z0-9][a-z0-9_-]{0,63}$", description: "Operator-configured service id" }; export const TOOL_DEFINITIONS = [ - { name: "list_services", description: "List the operator-configured development services and their current states.", inputSchema: { type: "object", additionalProperties: false } }, + { name: "list_services", description: "List operator-configured development services, their current states, and any agentInstructions that explain the files and verification required to use them.", inputSchema: { type: "object", additionalProperties: false } }, { name: "service_status", description: "Read one development service's state without changing it.", inputSchema: { type: "object", properties: { service: serviceProperty }, required: ["service"], additionalProperties: false } }, { name: "start_service", description: "Start one preconfigured development service. Commands and arguments cannot be supplied by the MCP caller.", inputSchema: { type: "object", properties: { service: serviceProperty }, required: ["service"], additionalProperties: false } }, { name: "stop_service", description: "Stop one development service and its process group.", inputSchema: { type: "object", properties: { service: serviceProperty }, required: ["service"], additionalProperties: false } }, diff --git a/dev-server-mcp/test/dev-server-service.test.js b/dev-server-mcp/test/dev-server-service.test.js index ec95de2..629cbff 100644 --- a/dev-server-mcp/test/dev-server-service.test.js +++ b/dev-server-mcp/test/dev-server-service.test.js @@ -36,6 +36,20 @@ test("configuration cannot replace the safe process environment", () => { } finally { item.cleanup(); } }); +test("list exposes an operator-written setup contract without exposing caller-controlled commands", () => { + const item = fixture({ services: { web: { + command: "node", args: ["server.js", "${port}"], cwd: ".", workspaceMode: "execution", + agentInstructions: "Create server.js, bind 0.0.0.0 and use process.env.PORT." + } } }); + try { + const config = loadDevServerConfig({ configPath: item.configPath, workspaceRoot: item.root, allowedCommands: ["node"] }); + const devServer = new DevServerService({ services: config.services }); + const listed = devServer.list({ key: "execution-1" }).services[0]; + assert.equal(listed.agentInstructions, "Create server.js, bind 0.0.0.0 and use process.env.PORT."); + assert.equal(Object.hasOwn(listed, "command"), false); + } finally { item.cleanup(); } +}); + test("dev server starts only configured arguments, captures logs, and stops the process group", async () => { const item = fixture({ services: { web: { command: process.execPath, args: ["-e", "console.log('ready'); setInterval(() => {}, 1000)"], cwd: ".", shutdownTimeoutMs: 500 } } }); try {