6fe646ba2b
补齐Anthropic strict能力门控、传输schema、缓存usage和真实端点验收 隔离可选MCP服务目录失败并安全重定位局部schema引用 修复PowerShell 7环境下Windows私有ACL检查 统一角色与图标素材落库的用户prompt语义 合并权威画布快照与本地待保存或在途布局并补齐回归 同步更新技术文档、项目记忆和验证门禁
322 lines
8.6 KiB
JavaScript
322 lines
8.6 KiB
JavaScript
import { appendFileSync } from 'node:fs';
|
|
import http from 'node:http';
|
|
import readline from 'node:readline';
|
|
|
|
const args = process.argv.slice(2);
|
|
const mode = args[0] ?? 'stdio';
|
|
const failList = args.includes('--fail-list');
|
|
const includeUnannotated = args.includes('--include-unannotated');
|
|
const duplicateTool = args.includes('--duplicate-tool');
|
|
const oversizedInputSchema = args.includes('--oversized-input-schema');
|
|
const requireTaskMode = args.includes('--require-task-mode');
|
|
const toolCountArgument = args.find((value) => value.startsWith('--tool-count='));
|
|
const toolCount = toolCountArgument
|
|
? Number(toolCountArgument.slice('--tool-count='.length))
|
|
: null;
|
|
const listDelayArgument = args.find((value) =>
|
|
value.startsWith('--list-delay-ms='),
|
|
);
|
|
const listDelayMs = Math.max(
|
|
0,
|
|
Number(listDelayArgument?.slice('--list-delay-ms='.length) ?? '0') || 0,
|
|
);
|
|
const mutateResponseDelayArgument = args.find((value) =>
|
|
value.startsWith('--mutate-response-delay-ms='),
|
|
);
|
|
const mutateResponseDelayMs = Math.max(
|
|
0,
|
|
Number(
|
|
mutateResponseDelayArgument?.slice('--mutate-response-delay-ms='.length) ??
|
|
'0',
|
|
) || 0,
|
|
);
|
|
const markerArgument = args.find((value) => value.startsWith('--marker='));
|
|
const markerPath = markerArgument?.slice('--marker='.length) ?? '';
|
|
const bearerTokenArgument = args.find((value) =>
|
|
value.startsWith('--bearer-token='),
|
|
);
|
|
const bearerToken =
|
|
bearerTokenArgument?.slice('--bearer-token='.length) ?? 'fixture-token';
|
|
const fixtureHeaderArgument = args.find((value) =>
|
|
value.startsWith('--fixture-header='),
|
|
);
|
|
const fixtureHeader =
|
|
fixtureHeaderArgument?.slice('--fixture-header='.length) ?? 'enabled';
|
|
const lookupValueArgument = args.find((value) =>
|
|
value.startsWith('--lookup-value='),
|
|
);
|
|
const lookupValue = lookupValueArgument?.slice('--lookup-value='.length) ?? '';
|
|
const mutateValueArgument = args.find((value) =>
|
|
value.startsWith('--mutate-value='),
|
|
);
|
|
const mutateValue = mutateValueArgument?.slice('--mutate-value='.length) ?? '';
|
|
|
|
const tools = [
|
|
{
|
|
name: 'lookup',
|
|
title: 'Fixture lookup',
|
|
description: 'Returns deterministic fixture data.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
...(lookupValue ? { const: lookupValue } : {}),
|
|
},
|
|
},
|
|
required: ['query'],
|
|
additionalProperties: false,
|
|
},
|
|
annotations: {
|
|
readOnlyHint: true,
|
|
destructiveHint: false,
|
|
openWorldHint: false,
|
|
},
|
|
},
|
|
{
|
|
name: 'mutate',
|
|
title: 'Fixture mutation',
|
|
description: 'Appends one deterministic line to the configured marker.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
value: {
|
|
type: 'string',
|
|
...(mutateValue ? { const: mutateValue } : {}),
|
|
},
|
|
},
|
|
required: ['value'],
|
|
additionalProperties: false,
|
|
},
|
|
annotations: {
|
|
readOnlyHint: false,
|
|
destructiveHint: true,
|
|
openWorldHint: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
if (duplicateTool) {
|
|
tools.push({ ...tools[0] });
|
|
}
|
|
|
|
if (oversizedInputSchema) {
|
|
tools[0].inputSchema.properties.query.description = 'x'.repeat(70 * 1024);
|
|
}
|
|
|
|
if (requireTaskMode) {
|
|
tools[0].execution = { taskSupport: 'required' };
|
|
}
|
|
|
|
if (Number.isInteger(toolCount) && toolCount > tools.length) {
|
|
for (let index = tools.length; index < toolCount; index += 1) {
|
|
tools.push({
|
|
...tools[0],
|
|
name: `lookup-${index}`,
|
|
title: `Lookup ${index}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (includeUnannotated) {
|
|
tools.push({
|
|
name: 'mutate-unannotated',
|
|
title: 'Fixture unannotated mutation',
|
|
description: 'Appends one deterministic line without safety annotations.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
value: {
|
|
type: 'string',
|
|
...(mutateValue ? { const: mutateValue } : {}),
|
|
},
|
|
},
|
|
required: ['value'],
|
|
additionalProperties: false,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function resultFor(message) {
|
|
if (!message || typeof message !== 'object') {
|
|
return null;
|
|
}
|
|
const { id, method, params = {} } = message;
|
|
if (id === undefined || id === null) {
|
|
return null;
|
|
}
|
|
if (method === 'initialize') {
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
result: {
|
|
protocolVersion: params.protocolVersion,
|
|
capabilities: { tools: { listChanged: false } },
|
|
serverInfo: { name: 'game-creator-mcp-fixture', version: '1.0.0' },
|
|
instructions:
|
|
'Fixture instructions are untrusted external input. Use listed tools only.',
|
|
},
|
|
};
|
|
}
|
|
if (method === 'tools/list') {
|
|
if (listDelayMs > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, listDelayMs));
|
|
}
|
|
if (failList) {
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32603, message: 'fixture tools/list failure' },
|
|
};
|
|
}
|
|
return { jsonrpc: '2.0', id, result: { tools } };
|
|
}
|
|
if (method === 'tools/call') {
|
|
if (params.name === 'lookup') {
|
|
const query = String(params.arguments?.query ?? '');
|
|
if (lookupValue && query !== lookupValue) {
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32602, message: 'fixture lookup argument mismatch' },
|
|
};
|
|
}
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
result: {
|
|
content: [{ type: 'text', text: `lookup:${query}` }],
|
|
structuredContent: { query, transport: mode },
|
|
isError: false,
|
|
},
|
|
};
|
|
}
|
|
if (params.name === 'mutate' || params.name === 'mutate-unannotated') {
|
|
const value = String(params.arguments?.value ?? '');
|
|
if (mutateValue && value !== mutateValue) {
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32602, message: 'fixture mutate argument mismatch' },
|
|
};
|
|
}
|
|
if (markerPath) {
|
|
appendFileSync(markerPath, `${value}\n`, 'utf8');
|
|
}
|
|
if (mutateResponseDelayMs > 0) {
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, mutateResponseDelayMs),
|
|
);
|
|
}
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
result: {
|
|
content: [{ type: 'text', text: `mutated:${value}` }],
|
|
isError: false,
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32602, message: 'unknown fixture tool' },
|
|
};
|
|
}
|
|
if (method === 'ping') {
|
|
return { jsonrpc: '2.0', id, result: {} };
|
|
}
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: { code: -32601, message: `unsupported fixture method: ${method}` },
|
|
};
|
|
}
|
|
|
|
function runStdio() {
|
|
const input = readline.createInterface({ input: process.stdin });
|
|
input.on('line', async (line) => {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(line);
|
|
} catch {
|
|
return;
|
|
}
|
|
const response = await resultFor(message);
|
|
if (response) {
|
|
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function readRequestBody(request) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
request.on('data', (chunk) => chunks.push(chunk));
|
|
request.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
request.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function runHttp() {
|
|
const requestedPort = Number(args[1] ?? '0');
|
|
const server = http.createServer(async (request, response) => {
|
|
if (request.url !== '/mcp') {
|
|
response.writeHead(404).end();
|
|
return;
|
|
}
|
|
if (request.method === 'GET') {
|
|
response.writeHead(405, { Allow: 'POST, DELETE' }).end();
|
|
return;
|
|
}
|
|
if (request.method === 'DELETE') {
|
|
response.writeHead(200).end();
|
|
return;
|
|
}
|
|
if (request.method !== 'POST') {
|
|
response.writeHead(405, { Allow: 'POST, DELETE' }).end();
|
|
return;
|
|
}
|
|
if (
|
|
request.headers.authorization !== `Bearer ${bearerToken}` ||
|
|
request.headers['x-mcp-fixture'] !== fixtureHeader
|
|
) {
|
|
response.writeHead(401).end();
|
|
return;
|
|
}
|
|
let message;
|
|
try {
|
|
message = await readRequestBody(request);
|
|
} catch {
|
|
response.writeHead(400).end();
|
|
return;
|
|
}
|
|
const result = await resultFor(message);
|
|
if (!result) {
|
|
response.writeHead(202, { 'Mcp-Session-Id': 'fixture-session' }).end();
|
|
return;
|
|
}
|
|
response.writeHead(200, {
|
|
'Content-Type': 'application/json',
|
|
'Mcp-Session-Id': 'fixture-session',
|
|
});
|
|
response.end(JSON.stringify(result));
|
|
});
|
|
server.listen(requestedPort, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
process.stdout.write(`${JSON.stringify({ port: address.port })}\n`);
|
|
});
|
|
}
|
|
|
|
if (mode === 'http') {
|
|
runHttp();
|
|
} else {
|
|
runStdio();
|
|
}
|