Improve MCP guidance and tool availability
This commit is contained in:
parent
1ca30a0ccf
commit
da7200a394
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`),
|
||||
|
|
|
|||
|
|
@ -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 } : {})
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } },
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue