123import { spawn } from 'node:child_process';4import { existsSync, readFileSync } from 'node:fs';5import { dirname, resolve } from 'node:path';6import { fileURLToPath } from 'node:url';7import readline from 'node:readline';89const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');10const repositorySkillPath = resolve(root, '../../skills/devsite-cli/SKILL.md');11const skillPath = existsSync(repositorySkillPath)12 ? repositorySkillPath13 : resolve(root, 'skills/devsite-cli/SKILL.md');14const binary = process.env.DEVSITE_BIN || 'devsite';15const protocolVersion = '2026-07-28';16const pluginManifest = JSON.parse(17 readFileSync(resolve(root, '.codex-plugin/plugin.json'), 'utf8'),18);19const serverInfo = { name: 'devsite', version: pluginManifest.version };20const serverMeta = { 'io.modelcontextprotocol/serverInfo': serverInfo };21const inFlight = new Map();22const cancelled = new Set();2324const tools = [25 {26 name: 'devsite_cli',27 description: 'Run one finite devsite CLI command in JSON mode. Resident connect/daemon commands are refused; supervise those directly in the harness.',28 inputSchema: {29 type: 'object',30 properties: {31 args: { type: 'array', items: { type: 'string' }, description: 'Arguments after devsite, without --json.' },32 },33 required: ['args'],34 additionalProperties: false,35 },36 annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },37 },38 {39 name: 'devsite_access_request',40 description: 'Create a signed, short-lived service request and a separate private requester endpoint key. Share only the request file.',41 inputSchema: {42 type: 'object',43 properties: {44 service: { type: 'string' },45 request_path: { type: 'string' },46 key_path: { type: 'string' },47 ttl: { type: 'integer', minimum: 1, maximum: 600, default: 300 },48 },49 required: ['service', 'request_path', 'key_path'],50 additionalProperties: false,51 },52 annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },53 },54 {55 name: 'devsite_access_resolve',56 description: 'Resolve a service keyword against services this scoped granting party credential may delegate.',57 inputSchema: {58 type: 'object',59 properties: { keyword: { type: 'string' } },60 required: ['keyword'],61 additionalProperties: false,62 },63 annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },64 },65 {66 name: 'devsite_access_grant',67 description: 'Plan or issue a short-lived endpoint-bound service grant. Set plan=true unless the exact grant is already authorized.',68 inputSchema: {69 type: 'object',70 properties: {71 request_path: { type: 'string' },72 resource_id: { type: 'string' },73 ttl: { type: 'integer', minimum: 1, maximum: 900, default: 900 },74 plan: { type: 'boolean', default: true },75 approved_plan: { type: 'string', description: 'Server-signed dsp_ token returned by the exact reviewed plan; required when plan=false.' },76 },77 required: ['request_path'],78 additionalProperties: false,79 },80 annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },81 },82];8384function commandWords(args) {85 const words = [];86 for (let index = 0; index < args.length; index += 1) {87 const word = args[index];88 if (word === '--json') continue;89 if (word === '--server') {90 index += 1;91 continue;92 }93 if (word.startsWith('--server=')) continue;94 words.push(word);95 }96 return words;97}9899async function runDevsite(args, requestId) {100 if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) {101 throw new Error('args must be an array of strings');102 }103 const words = args.filter((arg) => arg !== '--json');104 const command = commandWords(words);105 if (106 command[0] === 'connect'107 || (command[0] === 'daemon' && command[1] === 'run')108 || (command[0] === 'access' && command[1] === 'connect')109 ) {110 throw new Error('resident commands must be launched under the harness process supervisor');111 }112 if (command[0] === 'access' && command[1] === 'grant') {113 throw new Error('use devsite_access_grant so planning and approved-plan enforcement cannot be bypassed');114 }115 const result = await new Promise((resolveResult, rejectResult) => {116 const child = spawn(binary, ['--json', ...words], {117 env: process.env,118 shell: false,119 stdio: ['ignore', 'pipe', 'pipe'],120 });121 let stdout = '';122 let stderr = '';123 const limit = 4 * 1024 * 1024;124 const timeout = setTimeout(() => child.kill(), 30000);125 inFlight.set(requestId, child);126 child.stdout.setEncoding('utf8');127 child.stderr.setEncoding('utf8');128 child.stdout.on('data', (chunk) => {129 stdout += chunk;130 if (stdout.length > limit) child.kill();131 });132 child.stderr.on('data', (chunk) => {133 stderr += chunk;134 if (stderr.length > limit) child.kill();135 });136 child.on('error', rejectResult);137 child.on('close', (status, signal) => {138 clearTimeout(timeout);139 inFlight.delete(requestId);140 if (cancelled.has(requestId)) {141 rejectResult(new Error('request cancelled'));142 } else if (signal) {143 rejectResult(new Error(`devsite terminated by ${signal}`));144 } else {145 resolveResult({ stdout, stderr, status });146 }147 });148 });149 const stdout = result.stdout.trim();150 if (!stdout) throw new Error(result.stderr.trim() || `devsite exited ${result.status}`);151 let parsed;152 try {153 parsed = JSON.parse(stdout);154 } catch {155 throw new Error(`devsite returned non-JSON output: ${stdout.slice(0, 500)}`);156 }157 return parsed;158}159160async function callTool(name, input = {}, requestId) {161 switch (name) {162 case 'devsite_cli':163 return runDevsite(input.args, requestId);164 case 'devsite_access_request':165 return runDevsite([166 'access', 'request', input.service,167 '--request', input.request_path,168 '--key', input.key_path,169 '--ttl', String(input.ttl ?? 300),170 ], requestId);171 case 'devsite_access_resolve':172 return runDevsite(['access', 'resolve', input.keyword], requestId);173 case 'devsite_access_grant': {174 const args = [175 'access', 'grant', '--request', input.request_path,176 '--ttl', String(input.ttl ?? 900),177 ];178 if (input.resource_id) args.push('--resource', input.resource_id);179 if (input.plan !== false) {180 args.push('--plan');181 } else if (input.approved_plan) {182 args.push('--approved-plan', input.approved_plan);183 } else {184 throw new Error('approved_plan is required when plan=false');185 }186 return runDevsite(args, requestId);187 }188 default:189 throw new Error(`unknown tool ${name}`);190 }191}192193function result(id, value) {194 process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result: value })}\n`);195}196197function rpcError(id, code, message, data) {198 process.stdout.write(`${JSON.stringify({199 jsonrpc: '2.0',200 ...(typeof id === 'string' || typeof id === 'number' ? { id } : {}),201 error: { code, message, ...(data === undefined ? {} : { data }) },202 })}\n`);203}204205function requestProtocol(message) {206 return message.params?._meta?.['io.modelcontextprotocol/protocolVersion'];207}208209function validateModernRequest(message) {210 const requested = requestProtocol(message);211 if (typeof requested !== 'string') {212 rpcError(message.id, -32602, 'Missing per-request protocol version');213 return false;214 }215 if (requested !== protocolVersion) {216 rpcError(message.id, -32022, 'Unsupported protocol version', {217 supported: [protocolVersion],218 requested: requested ?? null,219 });220 return false;221 }222 const capabilities = message.params?._meta?.['io.modelcontextprotocol/clientCapabilities'];223 if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) {224 rpcError(message.id, -32602, 'Missing per-request client capabilities');225 return false;226 }227 return true;228}229230function complete(value) {231 return { resultType: 'complete', _meta: serverMeta, ...value };232}233234async function handle(message) {235 if (!message || typeof message !== 'object' || Array.isArray(message)) {236 rpcError(undefined, -32600, 'Invalid Request');237 return;238 }239 const { id, method, params = {} } = message;240 if (id === undefined) {241 if (method === 'notifications/cancelled') {242 const requestId = params.requestId;243 const child = inFlight.get(requestId);244 if (child) {245 cancelled.add(requestId);246 child.kill();247 }248 }249 return;250 }251 if (252 message.jsonrpc !== '2.0'253 || (typeof id !== 'string' && typeof id !== 'number')254 || typeof method !== 'string'255 || !params256 || typeof params !== 'object'257 || Array.isArray(params)258 ) {259 rpcError(id, -32600, 'Invalid Request');260 return;261 }262 if (!validateModernRequest(message)) return;263 try {264 switch (method) {265 case 'server/discover':266 result(id, complete({267 supportedVersions: [protocolVersion],268 capabilities: { tools: {}, resources: {} },269 serverInfo,270 instructions: 'Use the bundled devsite skill. Plan grant issuance before applying it, and never share requester endpoint keys.',271 ttlMs: 300000,272 cacheScope: 'public',273 }));274 break;275 case 'tools/list':276 result(id, complete({ tools, ttlMs: 300000, cacheScope: 'public' }));277 break;278 case 'tools/call': {279 if (!tools.some((tool) => tool.name === params.name)) {280 rpcError(id, -32602, `Unknown tool: ${params.name}`);281 break;282 }283 try {284 const value = await callTool(params.name, params.arguments, id);285 if (cancelled.delete(id)) break;286 result(id, complete({287 content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],288 structuredContent: value,289 isError: value?.ok === false,290 }));291 } catch (err) {292 if (cancelled.delete(id)) break;293 result(id, complete({294 content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],295 isError: true,296 }));297 }298 break;299 }300 case 'resources/list':301 result(id, complete({ resources: [302 { uri: 'devsite://skill', name: 'devsite CLI skill', mimeType: 'text/markdown' },303 { uri: 'devsite://help', name: 'installed devsite CLI help', mimeType: 'application/json' },304 ], ttlMs: 300000, cacheScope: 'public' }));305 break;306 case 'resources/read':307 if (params.uri === 'devsite://skill') {308 result(id, complete({309 contents: [{ uri: params.uri, mimeType: 'text/markdown', text: readFileSync(skillPath, 'utf8') }],310 ttlMs: 300000,311 cacheScope: 'public',312 }));313 } else if (params.uri === 'devsite://help') {314 result(id, complete({315 contents: [{ uri: params.uri, mimeType: 'application/json', text: JSON.stringify(await runDevsite(['--help'], id), null, 2) }],316 ttlMs: 60000,317 cacheScope: 'private',318 }));319 } else {320 rpcError(id, -32602, `Unknown resource: ${params.uri}`);321 }322 break;323 default:324 rpcError(id, -32601, `Method not found: ${method}`);325 }326 } catch (err) {327 rpcError(id, -32603, err instanceof Error ? err.message : String(err));328 }329}330331const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });332for await (const line of lines) {333 if (!line.trim()) continue;334 try {335 void handle(JSON.parse(line));336 } catch (err) {337 rpcError(undefined, -32700, err instanceof Error ? err.message : String(err));338 }339}