codex-octopus 1.0.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.
@@ -0,0 +1,153 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod/v4";
3
+ import { OPTION_CATALOG } from "../constants.js";
4
+ import {
5
+ sanitizeToolName,
6
+ deriveServerName,
7
+ deriveToolName,
8
+ serializeArrayEnv,
9
+ } from "../lib.js";
10
+
11
+ function buildEnvFromParams(
12
+ params: Record<string, unknown>
13
+ ): Record<string, string> {
14
+ const env: Record<string, string> = {};
15
+
16
+ for (const opt of OPTION_CATALOG) {
17
+ const val = params[opt.key];
18
+ if (val === undefined || val === null) continue;
19
+
20
+ if (Array.isArray(val)) {
21
+ if (val.length > 0) env[opt.envVar] = serializeArrayEnv(val);
22
+ } else if (typeof val === "object") {
23
+ env[opt.envVar] = JSON.stringify(val);
24
+ } else if (typeof val === "boolean") {
25
+ env[opt.envVar] = String(val);
26
+ } else if (val !== "") {
27
+ env[opt.envVar] = String(val);
28
+ }
29
+ }
30
+
31
+ return env;
32
+ }
33
+
34
+ export function registerFactoryTool(
35
+ server: McpServer,
36
+ ) {
37
+ server.registerTool("create_codex_mcp", {
38
+ description: [
39
+ "Generate a .mcp.json config entry for a new Codex Octopus MCP server instance.",
40
+ "WHEN TO USE: user says 'codex octopus agent', 'codex octopus mcp',",
41
+ "'new codex octopus', 'add codex octopus', 'create codex octopus',",
42
+ "'codex octopus instance', 'codex octopus config', 'codex octopus server',",
43
+ "or any phrase combining 'codex octopus' with agent/mcp/new/add/create/config/server/setup.",
44
+ "This is a wizard: only a description is required.",
45
+ "Returns a ready-to-use .mcp.json config and lists all customization options.",
46
+ "Call again with more parameters to refine.",
47
+ ].join(" "),
48
+ inputSchema: z.object({
49
+ description: z.string().describe(
50
+ "What this agent should do, in plain language. Example: 'a code reviewer with read-only sandbox'"
51
+ ),
52
+ name: z.string().optional().describe("Server name / alias (derived from description if omitted)"),
53
+ toolName: z.string().optional().describe("Custom tool name prefix"),
54
+ model: z.string().optional(),
55
+ instructions: z.string().optional(),
56
+ appendInstructions: z.string().optional(),
57
+ sandboxMode: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
58
+ approvalPolicy: z.enum(["never", "on-request", "on-failure", "untrusted"]).optional(),
59
+ cwd: z.string().optional(),
60
+ additionalDirs: z.array(z.string()).optional(),
61
+ effort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional(),
62
+ networkAccess: z.boolean().optional(),
63
+ webSearchMode: z.enum(["disabled", "cached", "live"]).optional(),
64
+ persistSession: z.boolean().optional(),
65
+ apiKey: z.string().optional().describe("OpenAI API key for this agent (leave unset to inherit)"),
66
+ }),
67
+ }, async (params) => {
68
+ const { description, name: nameParam, toolName: toolNameParam } = params;
69
+
70
+ const name = nameParam || deriveServerName(description);
71
+ const derivedToolName = toolNameParam
72
+ ? sanitizeToolName(toolNameParam)
73
+ : deriveToolName(name);
74
+
75
+ const env: Record<string, string> = {
76
+ CODEX_TOOL_NAME: derivedToolName,
77
+ CODEX_SERVER_NAME: name,
78
+ CODEX_DESCRIPTION: description,
79
+ };
80
+
81
+ const optionEnv = buildEnvFromParams(params);
82
+ Object.assign(env, optionEnv);
83
+
84
+ const configured = Object.keys(optionEnv);
85
+ const notConfigured = OPTION_CATALOG.filter(
86
+ (o) => !configured.includes(o.envVar)
87
+ );
88
+
89
+ const SENSITIVE_KEYS = new Set(["CODEX_API_KEY"]);
90
+
91
+ const displayEnv: Record<string, string> = {};
92
+ for (const [k, v] of Object.entries(env)) {
93
+ displayEnv[k] = SENSITIVE_KEYS.has(k) ? "<REDACTED>" : v;
94
+ }
95
+
96
+ const mcpEntry = {
97
+ [name]: {
98
+ command: "npx",
99
+ args: ["codex-octopus"],
100
+ env: displayEnv,
101
+ },
102
+ };
103
+
104
+ const sections: string[] = [];
105
+
106
+ sections.push(
107
+ "## Generated config",
108
+ "",
109
+ "Add to `mcpServers` in your `.mcp.json`:",
110
+ "",
111
+ "```json",
112
+ JSON.stringify(mcpEntry, null, 2),
113
+ "```"
114
+ );
115
+
116
+ sections.push("", "## What's configured");
117
+ sections.push("", `| Setting | Value |`, `|---|---|`);
118
+ sections.push(`| Name | \`${name}\` |`);
119
+ sections.push(`| Tool names | \`${derivedToolName}\`, \`${derivedToolName}_reply\` |`);
120
+ for (const key of configured) {
121
+ const opt = OPTION_CATALOG.find((o) => o.envVar === key);
122
+ if (opt) {
123
+ const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
124
+ sections.push(`| ${opt.label} | \`${val}\` |`);
125
+ }
126
+ }
127
+ if (configured.length === 0) {
128
+ sections.push(`| _(all defaults)_ | — |`);
129
+ }
130
+
131
+ if (notConfigured.length > 0) {
132
+ sections.push(
133
+ "",
134
+ "## Available customizations",
135
+ "",
136
+ "These options are not yet set. Ask the user if they'd like to configure any:",
137
+ ""
138
+ );
139
+ for (const opt of notConfigured) {
140
+ sections.push(`- **${opt.label}** — ${opt.hint}`);
141
+ sections.push(` Example: ${opt.example}`);
142
+ }
143
+ sections.push(
144
+ "",
145
+ "_Call this tool again with additional parameters to refine the config._"
146
+ );
147
+ }
148
+
149
+ return {
150
+ content: [{ type: "text" as const, text: sections.join("\n") }],
151
+ };
152
+ });
153
+ }
@@ -0,0 +1,200 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod/v4";
3
+ import { resolve } from "node:path";
4
+ import { Codex } from "@openai/codex-sdk";
5
+ import type { ThreadConfig, InvocationOverrides } from "../types.js";
6
+ import {
7
+ narrowSandboxMode,
8
+ narrowApprovalPolicy,
9
+ formatErrorMessage,
10
+ } from "../lib.js";
11
+
12
+ interface RunResult {
13
+ threadId: string;
14
+ response: string;
15
+ usage: { input_tokens: number; cached_input_tokens: number; output_tokens: number };
16
+ isError: boolean;
17
+ }
18
+
19
+ async function runQuery(
20
+ prompt: string,
21
+ overrides: InvocationOverrides,
22
+ baseConfig: ThreadConfig,
23
+ apiKey?: string
24
+ ): Promise<RunResult> {
25
+ const codex = new Codex({
26
+ ...(apiKey ? { apiKey } : {}),
27
+ });
28
+
29
+ const threadOptions: Record<string, unknown> = {};
30
+
31
+ // Model
32
+ const model = overrides.model || baseConfig.model;
33
+ if (model) threadOptions.model = model;
34
+
35
+ // Working directory — accept any path, preserve agent's base access
36
+ let cwd = baseConfig.cwd || process.cwd();
37
+ if (overrides.cwd) {
38
+ const resolvedCwd = resolve(cwd, overrides.cwd);
39
+ if (resolvedCwd !== cwd) {
40
+ const dirs = new Set(baseConfig.additionalDirectories || []);
41
+ dirs.add(cwd);
42
+ threadOptions.additionalDirectories = [...dirs];
43
+ cwd = resolvedCwd;
44
+ }
45
+ }
46
+ threadOptions.workingDirectory = cwd;
47
+
48
+ // Per-invocation additionalDirs — unions with server-level + auto-added dirs
49
+ if (overrides.additionalDirs?.length) {
50
+ const existing = (threadOptions.additionalDirectories as string[]) || baseConfig.additionalDirectories || [];
51
+ const dirs = new Set(existing);
52
+ for (const dir of overrides.additionalDirs) {
53
+ dirs.add(dir);
54
+ }
55
+ threadOptions.additionalDirectories = [...dirs];
56
+ } else if (baseConfig.additionalDirectories && !threadOptions.additionalDirectories) {
57
+ threadOptions.additionalDirectories = baseConfig.additionalDirectories;
58
+ }
59
+
60
+ // Sandbox mode — can only tighten
61
+ const baseSandbox = baseConfig.sandboxMode || "read-only";
62
+ if (overrides.sandboxMode) {
63
+ threadOptions.sandboxMode = narrowSandboxMode(baseSandbox, overrides.sandboxMode);
64
+ } else {
65
+ threadOptions.sandboxMode = baseSandbox;
66
+ }
67
+
68
+ // Approval policy — can only tighten
69
+ const baseApproval = baseConfig.approvalPolicy || "on-failure";
70
+ if (overrides.approvalPolicy) {
71
+ threadOptions.approvalPolicy = narrowApprovalPolicy(baseApproval, overrides.approvalPolicy);
72
+ } else {
73
+ threadOptions.approvalPolicy = baseApproval;
74
+ }
75
+
76
+ // Effort
77
+ const effort = overrides.effort || baseConfig.effort;
78
+ if (effort) threadOptions.modelReasoningEffort = effort;
79
+
80
+ // Network access
81
+ if (overrides.networkAccess !== undefined) {
82
+ threadOptions.networkAccessEnabled = overrides.networkAccess;
83
+ } else if (baseConfig.networkAccess !== undefined) {
84
+ threadOptions.networkAccessEnabled = baseConfig.networkAccess;
85
+ }
86
+
87
+ // Web search
88
+ const webSearch = overrides.webSearchMode || baseConfig.webSearchMode;
89
+ if (webSearch) threadOptions.webSearchMode = webSearch;
90
+
91
+ // Start or resume thread
92
+ const thread = overrides.resumeThreadId
93
+ ? codex.resumeThread(overrides.resumeThreadId, threadOptions)
94
+ : codex.startThread(threadOptions);
95
+
96
+ // Build prompt with instructions
97
+ let fullPrompt = prompt;
98
+ if (!overrides.resumeThreadId) {
99
+ const instructions = overrides.instructions || baseConfig.instructions;
100
+ const appendInstructions = baseConfig.appendInstructions;
101
+ if (instructions) {
102
+ fullPrompt = `${instructions}\n\n${prompt}`;
103
+ } else if (appendInstructions) {
104
+ fullPrompt = `${appendInstructions}\n\n${prompt}`;
105
+ }
106
+ }
107
+
108
+ const turn = await thread.run(fullPrompt);
109
+
110
+ return {
111
+ threadId: thread.id || "",
112
+ response: turn.finalResponse || "",
113
+ usage: turn.usage || { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 },
114
+ isError: false,
115
+ };
116
+ }
117
+
118
+ function formatResult(result: RunResult) {
119
+ const payload = {
120
+ thread_id: result.threadId,
121
+ result: result.response,
122
+ usage: result.usage,
123
+ is_error: result.isError,
124
+ };
125
+ return {
126
+ content: [
127
+ { type: "text" as const, text: JSON.stringify(payload, null, 2) },
128
+ ],
129
+ isError: result.isError,
130
+ };
131
+ }
132
+
133
+ function formatError(error: unknown) {
134
+ return {
135
+ content: [
136
+ { type: "text" as const, text: `Error: ${formatErrorMessage(error)}` },
137
+ ],
138
+ isError: true,
139
+ };
140
+ }
141
+
142
+ export function registerQueryTools(
143
+ server: McpServer,
144
+ baseConfig: ThreadConfig,
145
+ toolName: string,
146
+ toolDescription: string,
147
+ apiKey?: string
148
+ ) {
149
+ const replyToolName = `${toolName}_reply`;
150
+
151
+ server.registerTool(toolName, {
152
+ description: toolDescription,
153
+ inputSchema: z.object({
154
+ prompt: z.string().describe("Task or question for Codex"),
155
+ cwd: z.string().optional().describe("Working directory (overrides CODEX_CWD)"),
156
+ model: z.string().optional().describe('Model override (e.g. "gpt-5-codex", "o3", "codex-1")'),
157
+ additionalDirs: z.array(z.string()).optional().describe("Extra directories the agent can access for this invocation"),
158
+ effort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional().describe("Reasoning effort override"),
159
+ sandboxMode: z.enum(["read-only", "workspace-write"]).optional().describe("Sandbox mode override (can only tighten, never loosen)"),
160
+ approvalPolicy: z.enum(["on-failure", "on-request", "untrusted"]).optional().describe("Approval policy override (can only tighten, never loosen)"),
161
+ networkAccess: z.boolean().optional().describe("Enable network access from sandbox"),
162
+ webSearchMode: z.enum(["disabled", "cached", "live"]).optional().describe("Web search mode"),
163
+ instructions: z.string().optional().describe("Additional instructions (prepended to prompt)"),
164
+ }),
165
+ }, async ({ prompt, cwd, model, additionalDirs, effort, sandboxMode, approvalPolicy, networkAccess, webSearchMode, instructions }) => {
166
+ try {
167
+ const result = await runQuery(prompt, {
168
+ cwd, model, additionalDirs, effort, sandboxMode, approvalPolicy, networkAccess, webSearchMode, instructions,
169
+ }, baseConfig, apiKey);
170
+ return formatResult(result);
171
+ } catch (error) {
172
+ return formatError(error);
173
+ }
174
+ });
175
+
176
+ if (baseConfig.persistSession !== false) {
177
+ server.registerTool(replyToolName, {
178
+ description: [
179
+ `Continue a previous ${toolName} conversation by thread ID.`,
180
+ "Use this for follow-up questions, iterative refinement,",
181
+ "or multi-step workflows that build on prior context.",
182
+ ].join(" "),
183
+ inputSchema: z.object({
184
+ thread_id: z.string().describe(`Thread ID from a prior ${toolName} response`),
185
+ prompt: z.string().describe("Follow-up instruction or question"),
186
+ cwd: z.string().optional().describe("Working directory override"),
187
+ model: z.string().optional().describe("Model override"),
188
+ }),
189
+ }, async ({ thread_id, prompt, cwd, model }) => {
190
+ try {
191
+ const result = await runQuery(prompt, {
192
+ cwd, model, resumeThreadId: thread_id,
193
+ }, baseConfig, apiKey);
194
+ return formatResult(result);
195
+ } catch (error) {
196
+ return formatError(error);
197
+ }
198
+ });
199
+ }
200
+ }
package/src/types.ts ADDED
@@ -0,0 +1,34 @@
1
+ export interface ThreadConfig {
2
+ cwd?: string;
3
+ model?: string;
4
+ sandboxMode?: "read-only" | "workspace-write" | "danger-full-access";
5
+ approvalPolicy?: "never" | "on-request" | "on-failure" | "untrusted";
6
+ effort?: "minimal" | "low" | "medium" | "high" | "xhigh";
7
+ additionalDirectories?: string[];
8
+ networkAccess?: boolean;
9
+ webSearchMode?: "disabled" | "cached" | "live";
10
+ persistSession?: boolean;
11
+ instructions?: string;
12
+ appendInstructions?: string;
13
+ }
14
+
15
+ export interface InvocationOverrides {
16
+ cwd?: string;
17
+ model?: string;
18
+ sandboxMode?: string;
19
+ approvalPolicy?: string;
20
+ effort?: string;
21
+ additionalDirs?: string[];
22
+ networkAccess?: boolean;
23
+ webSearchMode?: string;
24
+ instructions?: string;
25
+ resumeThreadId?: string;
26
+ }
27
+
28
+ export interface OptionCatalogEntry {
29
+ key: string;
30
+ envVar: string;
31
+ label: string;
32
+ hint: string;
33
+ example: string;
34
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"],
14
+ "exclude": ["src/**/*.test.ts"]
15
+ }