Skip to content

Commit 3e1fa74

Browse files
author
NagyVikt
committed
Keep Colony MCP alive through stdio initialize
Constraint: Codex reports startup failure when the local Colony CLI exits before MCP initialize completes. Rejected: Disable the Colony MCP | hides the integration instead of repairing the local server. Confidence: high Scope-risk: narrow Directive: Keep the CLI entrypoint awaited and treat stdio MCP startup as process-lifecycle-sensitive. Tested: pnpm --filter colonyq typecheck; pnpm --filter @colony/mcp-server typecheck; pnpm --filter colonyq build; pnpm --filter @colony/mcp-server build; colony gain --summary; raw colony mcp initialize. Not-tested: Full workspace test suite.
1 parent ac75640 commit 3e1fa74

2 files changed

Lines changed: 55 additions & 3 deletions

File tree

apps/cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ if (isMainEntry()) {
126126
if (err.code === 'EPIPE') process.exit(0);
127127
throw err;
128128
});
129-
createProgram()
129+
await createProgram()
130130
.parseAsync(process.argv)
131131
.catch((err) => {
132132
process.stderr.write(`colony error: ${err instanceof Error ? err.message : String(err)}\n`);

apps/mcp-server/src/server.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/usr/bin/env node
2+
import { readSync } from 'node:fs';
23
import { join } from 'node:path';
4+
import { PassThrough, type Readable } from 'node:stream';
35
import { type Settings, loadSettings, resolveDataDir } from '@colony/config';
46
import { type Embedder, MemoryStore } from '@colony/core';
57
import { createEmbedder } from '@colony/embedding';
@@ -149,8 +151,58 @@ export async function main(): Promise<void> {
149151
const store = new MemoryStore({ dbPath, settings });
150152

151153
const server = buildServer(store, settings);
152-
const transport = new StdioServerTransport();
153-
await server.connect(transport);
154+
const input = createMcpInput();
155+
const transport = new StdioServerTransport(input, process.stdout);
156+
try {
157+
await server.connect(transport);
158+
await waitForInputClose(input);
159+
} finally {
160+
store.close();
161+
}
162+
}
163+
164+
function createMcpInput(): Readable {
165+
if (process.stdin.isTTY) return process.stdin;
166+
167+
const input = new PassThrough();
168+
const firstChunk = Buffer.alloc(64 * 1024);
169+
const bytesRead = readSync(0, firstChunk, 0, firstChunk.length, null);
170+
171+
if (bytesRead === 0) {
172+
input.end();
173+
return input;
174+
}
175+
176+
input.write(firstChunk.subarray(0, bytesRead));
177+
process.stdin.pipe(input);
178+
return input;
179+
}
180+
181+
function waitForInputClose(input: Readable): Promise<void> {
182+
if (input.destroyed || input.readableEnded) return Promise.resolve();
183+
184+
return new Promise((resolve) => {
185+
const keepAlive = setInterval(() => {}, 1_000_000_000);
186+
const done = () => {
187+
clearInterval(keepAlive);
188+
process.off('SIGTERM', done);
189+
process.off('SIGINT', done);
190+
input.off('end', done);
191+
input.off('close', done);
192+
input.off('error', done);
193+
resolve();
194+
};
195+
196+
input.once('end', done);
197+
input.once('close', done);
198+
input.once('error', done);
199+
process.once('SIGTERM', done);
200+
process.once('SIGINT', done);
201+
202+
// A stdio MCP server must keep the process alive after connect() returns.
203+
// The stdin stream alone is not a reliable event-loop ref in spawned MCP
204+
// clients. The MCP client owns shutdown by closing/killing the child.
205+
});
154206
}
155207

156208
if (isMainEntry(import.meta.url)) {

0 commit comments

Comments
 (0)