@wyrd-company/async-codex-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  const options = parseArgs(process.argv.slice(2));
6
- const server = new McpServer({ name: "async-codex-mcp-callback", version: "0.1.0" }, {
6
+ const server = new McpServer({ name: "async-codex-mcp-callback", version: "0.2.0" }, {
7
7
  instructions: "Use async_codex_ask_user only when you need a user answer before continuing. Use async_codex_notify_user for non-blocking progress updates or FYIs.",
8
8
  });
9
9
  server.tool("async_codex_ask_user", "Ask the user a blocking question. Codex waits until the user responds to the async session.", {
@@ -17,6 +17,7 @@ export declare class CodexMcpClient implements CodexClientLike {
17
17
  constructor(config: AsyncCodexConfig);
18
18
  callCodex(profile: ToolProfile, args: CodexToolArguments): Promise<CallToolResult>;
19
19
  continueSession(sessionId: string, prompt: string, cwd?: string): Promise<CallToolResult>;
20
+ private requestOptions;
20
21
  close(): Promise<void>;
21
22
  private getClient;
22
23
  }
@@ -27,14 +27,19 @@ export class CodexMcpClient {
27
27
  codexArgs["developer-instructions"] = profile.developerInstructions;
28
28
  if (Object.keys(profile.config).length > 0)
29
29
  codexArgs.config = profile.config;
30
- return client.callTool({ name: "codex", arguments: codexArgs });
30
+ return client.callTool({ name: "codex", arguments: codexArgs }, undefined, this.requestOptions());
31
31
  }
32
32
  async continueSession(sessionId, prompt, cwd) {
33
33
  const client = await this.getClient();
34
34
  const args = { threadId: sessionId, prompt };
35
35
  if (cwd)
36
36
  args.cwd = cwd;
37
- return client.callTool({ name: "codex-reply", arguments: args });
37
+ return client.callTool({ name: "codex-reply", arguments: args }, undefined, this.requestOptions());
38
+ }
39
+ // The SDK default request timeout is 60s, which aborts any Codex run
40
+ // longer than a minute regardless of callback state.
41
+ requestOptions() {
42
+ return { timeout: this.config.codex.requestTimeoutSec * 1000, resetTimeoutOnProgress: true };
38
43
  }
39
44
  async close() {
40
45
  await this.client?.close();
@@ -45,7 +50,7 @@ export class CodexMcpClient {
45
50
  async getClient() {
46
51
  if (this.client)
47
52
  return this.client;
48
- this.client = new Client({ name: "async-codex-mcp-client", version: "0.1.0" });
53
+ this.client = new Client({ name: "async-codex-mcp-client", version: "0.2.0" });
49
54
  this.transport = new StdioClientTransport({
50
55
  command: this.config.codex.command,
51
56
  args: this.config.codex.args,
@@ -9,7 +9,8 @@ declare const profileSchema: z.ZodObject<{
9
9
  developerInstructions: z.ZodOptional<z.ZodString>;
10
10
  config: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
11
  callbacks: z.ZodOptional<z.ZodObject<{
12
- enabled: z.ZodBoolean;
12
+ enabled: z.ZodOptional<z.ZodBoolean>;
13
+ askTimeoutSec: z.ZodOptional<z.ZodNumber>;
13
14
  }, z.core.$strict>>;
14
15
  }, z.core.$strict>;
15
16
  declare const configSchema: z.ZodObject<{
@@ -18,9 +19,11 @@ declare const configSchema: z.ZodObject<{
18
19
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
19
20
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
20
21
  cwd: z.ZodOptional<z.ZodString>;
22
+ requestTimeoutSec: z.ZodDefault<z.ZodNumber>;
21
23
  }, z.core.$strip>>;
22
24
  callbacks: z.ZodDefault<z.ZodObject<{
23
25
  enabled: z.ZodDefault<z.ZodBoolean>;
26
+ askTimeoutSec: z.ZodDefault<z.ZodNumber>;
24
27
  }, z.core.$strict>>;
25
28
  tools: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
26
29
  description: z.ZodOptional<z.ZodString>;
@@ -32,7 +35,8 @@ declare const configSchema: z.ZodObject<{
32
35
  developerInstructions: z.ZodOptional<z.ZodString>;
33
36
  config: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
37
  callbacks: z.ZodOptional<z.ZodObject<{
35
- enabled: z.ZodBoolean;
38
+ enabled: z.ZodOptional<z.ZodBoolean>;
39
+ askTimeoutSec: z.ZodOptional<z.ZodNumber>;
36
40
  }, z.core.$strict>>;
37
41
  }, z.core.$strict>>>;
38
42
  }, z.core.$strip>;
@@ -9,12 +9,15 @@ const codexServerSchema = z.object({
9
9
  args: z.array(z.string()).default(["mcp-server"]),
10
10
  env: stringRecordSchema.default({}),
11
11
  cwd: z.string().optional(),
12
+ requestTimeoutSec: z.number().int().positive().default(86400),
12
13
  });
13
14
  const callbacksSchema = z.object({
14
15
  enabled: z.boolean().default(true),
16
+ askTimeoutSec: z.number().int().positive().default(3600),
15
17
  }).strict();
16
18
  const profileCallbacksSchema = z.object({
17
- enabled: z.boolean(),
19
+ enabled: z.boolean().optional(),
20
+ askTimeoutSec: z.number().int().positive().optional(),
18
21
  }).strict();
19
22
  const profileSchema = z.object({
20
23
  description: z.string().optional(),
@@ -28,8 +31,8 @@ const profileSchema = z.object({
28
31
  callbacks: profileCallbacksSchema.optional(),
29
32
  }).strict();
30
33
  const configSchema = z.object({
31
- codex: codexServerSchema.default({ command: "codex", args: ["mcp-server"], env: {} }),
32
- callbacks: callbacksSchema.default({ enabled: true }),
34
+ codex: codexServerSchema.default({ command: "codex", args: ["mcp-server"], env: {}, requestTimeoutSec: 86400 }),
35
+ callbacks: callbacksSchema.default({ enabled: true, askTimeoutSec: 3600 }),
33
36
  tools: z.record(z.string(), profileSchema).default({
34
37
  codex: {
35
38
  description: "Run Codex asynchronously with danger-full-access sandboxing.",
@@ -20,9 +20,12 @@ const answerShape = {
20
20
  message: z.string().min(1).describe("User response to return to Codex."),
21
21
  };
22
22
  export function createServer(config, options = {}) {
23
- const server = new McpServer({ name: "async-codex-mcp", version: "0.1.0" }, {
24
- capabilities: { logging: {} },
25
- instructions: "Starts Codex sub-agent sessions asynchronously. Profile tools return immediately with an async session id; use continue-session after completion to resume.",
23
+ const server = new McpServer({ name: "async-codex-mcp", version: "0.2.0" }, {
24
+ capabilities: { logging: {}, experimental: { "claude/channel": {} } },
25
+ instructions: "Starts Codex sub-agent sessions asynchronously. Profile tools return immediately with an async session id; use continue-session after completion to resume. " +
26
+ 'When this server runs as a Claude Code channel, session events arrive as <channel source="async-codex-mcp" session_id="..." kind="...">. ' +
27
+ "kind=ask means Codex is blocked waiting for input: call answer-session with the session_id from the tag. " +
28
+ "kind=notify is a non-blocking progress update. kind=completed or kind=failed means the session finished; use session-status or continue-session.",
26
29
  });
27
30
  const client = options.client ?? new CodexMcpClient(config);
28
31
  const store = options.store ?? new SessionStore();
@@ -155,12 +158,22 @@ function errorMessageFromResult(result) {
155
158
  .trim();
156
159
  return text || "Codex returned an error result.";
157
160
  }
161
+ async function sendChannelNotification(server, content, meta) {
162
+ const cleanMeta = Object.fromEntries(Object.entries(meta).filter((entry) => typeof entry[1] === "string"));
163
+ await server.server.notification({
164
+ method: "notifications/claude/channel",
165
+ params: { content, meta: cleanMeta },
166
+ });
167
+ }
158
168
  async function sendSessionNotification(server, sessionId, status, codexSessionId, error) {
159
169
  await server.server.sendLoggingMessage({
160
170
  level: status === "completed" ? "notice" : "error",
161
171
  logger: "async-codex-mcp",
162
172
  data: { session_id: sessionId, status, codex_session_id: codexSessionId, error },
163
173
  });
174
+ await sendChannelNotification(server, status === "completed"
175
+ ? `Async Codex session ${sessionId} completed. Use session-status to read the result or continue-session to resume.`
176
+ : `Async Codex session ${sessionId} failed: ${error ?? "unknown error"}`, { session_id: sessionId, kind: status, codex_session_id: codexSessionId });
164
177
  }
165
178
  async function sendCallbackNotification(server, sessionId, type, message, extra) {
166
179
  await server.server.sendLoggingMessage({
@@ -168,6 +181,7 @@ async function sendCallbackNotification(server, sessionId, type, message, extra)
168
181
  logger: "async-codex-mcp",
169
182
  data: { session_id: sessionId, type, message, ...extra },
170
183
  });
184
+ await sendChannelNotification(server, extra.context ? `${message}\n\nContext: ${extra.context}` : message, { session_id: sessionId, kind: type, topic: extra.topic });
171
185
  }
172
186
  async function prepareProfile(config, profile, sessionId, callbackHub) {
173
187
  if (!callbacksEnabled(config, profile)) {
@@ -192,6 +206,9 @@ async function prepareProfile(config, profile, sessionId, callbackHub) {
192
206
  "--session-id",
193
207
  sessionId,
194
208
  ],
209
+ // Codex aborts blocked ask_user calls at its default MCP tool
210
+ // timeout (60s); a human answer routinely takes longer.
211
+ tool_timeout_sec: profile.callbacks?.askTimeoutSec ?? config.callbacks.askTimeoutSec,
195
212
  },
196
213
  },
197
214
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyrd-company/async-codex-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Async MCP proxy for Codex MCP server profiles",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,14 +12,18 @@
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.json",
15
- "prepack": "npm run build",
15
+ "build:bundle": "esbuild src/cli.ts src/callback-cli.ts --bundle --platform=node --target=node20 --format=esm --outdir=dist/bundle --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\"",
16
+ "prepack": "npm run build && npm run build:bundle",
16
17
  "test": "vitest run",
17
18
  "start": "node dist/src/cli.js"
18
19
  },
19
20
  "files": [
21
+ ".claude-plugin",
22
+ ".mcp.json",
23
+ "bin",
20
24
  "dist/src",
21
- "fixtures",
22
- "plugins/async-codex-mcp"
25
+ "dist/bundle",
26
+ "fixtures"
23
27
  ],
24
28
  "repository": {
25
29
  "type": "git",
@@ -38,6 +42,7 @@
38
42
  "@openai/codex": "^0.135.0",
39
43
  "@types/js-yaml": "^4.0.9",
40
44
  "@types/node": "^22.15.3",
45
+ "esbuild": "^0.25.0",
41
46
  "mcp-testing-kit": "github:thoughtspot/mcp-testing-kit",
42
47
  "typescript": "^5.8.3",
43
48
  "vitest": "^3.1.2"