claude-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,150 @@
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
+ serverEntry: string
37
+ ) {
38
+ server.registerTool("create_claude_code_mcp", {
39
+ description: [
40
+ "Create a new specialized Claude Code agent as an MCP server.",
41
+ "WHEN TO USE: user says 'create agent', 'new agent', 'set up agent',",
42
+ "'configure agent', 'add a reviewer', 'add a test writer',",
43
+ "'make me a code reviewer', 'I need a specialized agent', etc.",
44
+ "DO NOT USE when user just wants to run a task — use the main tool for that.",
45
+ "This is a wizard: only a description is required.",
46
+ "Returns a ready-to-use .mcp.json config and lists all customization options.",
47
+ "Call again with more parameters to refine.",
48
+ ].join(" "),
49
+ inputSchema: z.object({
50
+ description: z.string().describe(
51
+ "What this agent should do, in plain language. Example: 'a strict code reviewer that only reads files'"
52
+ ),
53
+ name: z.string().optional().describe("Server name / alias (derived from description if omitted)"),
54
+ toolName: z.string().optional().describe("Custom tool name prefix"),
55
+ model: z.string().optional(),
56
+ systemPrompt: z.string().optional(),
57
+ appendPrompt: z.string().optional(),
58
+ allowedTools: z.array(z.string()).optional(),
59
+ disallowedTools: z.array(z.string()).optional(),
60
+ cwd: z.string().optional(),
61
+ additionalDirs: z.array(z.string()).optional(),
62
+ plugins: z.array(z.string()).optional(),
63
+ maxTurns: z.number().optional(),
64
+ maxBudgetUsd: z.number().optional(),
65
+ effort: z.enum(["low", "medium", "high", "max"]).optional(),
66
+ persistSession: z.boolean().optional(),
67
+ permissionMode: z.enum(["bypassPermissions", "acceptEdits", "default", "plan"]).optional(),
68
+ settingSources: z.array(z.string()).optional(),
69
+ mcpServers: z.record(z.string(), z.unknown()).optional(),
70
+ betas: z.array(z.string()).optional(),
71
+ }),
72
+ }, async (params) => {
73
+ const { description, name: nameParam, toolName: toolNameParam } = params;
74
+
75
+ const name = nameParam || deriveServerName(description);
76
+ const derivedToolName = toolNameParam
77
+ ? sanitizeToolName(toolNameParam)
78
+ : deriveToolName(name);
79
+
80
+ const env: Record<string, string> = {
81
+ CLAUDE_TOOL_NAME: derivedToolName,
82
+ CLAUDE_SERVER_NAME: name,
83
+ CLAUDE_DESCRIPTION: description,
84
+ };
85
+
86
+ const optionEnv = buildEnvFromParams(params);
87
+ Object.assign(env, optionEnv);
88
+
89
+ const configured = Object.keys(optionEnv);
90
+ const notConfigured = OPTION_CATALOG.filter(
91
+ (o) => !configured.includes(o.envVar)
92
+ );
93
+
94
+ const mcpEntry = {
95
+ [name]: {
96
+ command: "node",
97
+ args: [serverEntry],
98
+ env,
99
+ },
100
+ };
101
+
102
+ const sections: string[] = [];
103
+
104
+ sections.push(
105
+ "## Generated config",
106
+ "",
107
+ "Add to `mcpServers` in your `.mcp.json`:",
108
+ "",
109
+ "```json",
110
+ JSON.stringify(mcpEntry, null, 2),
111
+ "```"
112
+ );
113
+
114
+ sections.push("", "## What's configured");
115
+ sections.push("", `| Setting | Value |`, `|---|---|`);
116
+ sections.push(`| Name | \`${name}\` |`);
117
+ sections.push(`| Tool names | \`${derivedToolName}\`, \`${derivedToolName}_reply\` |`);
118
+ for (const key of configured) {
119
+ const opt = OPTION_CATALOG.find((o) => o.envVar === key);
120
+ if (opt) {
121
+ sections.push(`| ${opt.label} | \`${env[key]}\` |`);
122
+ }
123
+ }
124
+ if (configured.length === 0) {
125
+ sections.push(`| _(all defaults)_ | — |`);
126
+ }
127
+
128
+ if (notConfigured.length > 0) {
129
+ sections.push(
130
+ "",
131
+ "## Available customizations",
132
+ "",
133
+ "These options are not yet set. Ask the user if they'd like to configure any:",
134
+ ""
135
+ );
136
+ for (const opt of notConfigured) {
137
+ sections.push(`- **${opt.label}** — ${opt.hint}`);
138
+ sections.push(` Example: ${opt.example}`);
139
+ }
140
+ sections.push(
141
+ "",
142
+ "_Call this tool again with additional parameters to refine the config._"
143
+ );
144
+ }
145
+
146
+ return {
147
+ content: [{ type: "text" as const, text: sections.join("\n") }],
148
+ };
149
+ });
150
+ }
@@ -0,0 +1,171 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod/v4";
3
+ import { resolve } from "node:path";
4
+ import {
5
+ query,
6
+ type SDKResultMessage,
7
+ } from "@anthropic-ai/claude-agent-sdk";
8
+ import type { Options, InvocationOverrides } from "../types.js";
9
+ import {
10
+ isDescendantPath,
11
+ mergeAllowedTools,
12
+ mergeDisallowedTools,
13
+ buildResultPayload,
14
+ formatErrorMessage,
15
+ } from "../lib.js";
16
+
17
+ async function runQuery(
18
+ prompt: string,
19
+ overrides: InvocationOverrides,
20
+ baseOptions: Options
21
+ ): Promise<SDKResultMessage> {
22
+ const options: Options = { ...baseOptions };
23
+
24
+ if (overrides.cwd) {
25
+ const baseCwd = baseOptions.cwd || process.cwd();
26
+ if (isDescendantPath(overrides.cwd, baseCwd)) {
27
+ options.cwd = resolve(baseCwd, overrides.cwd);
28
+ }
29
+ }
30
+ if (overrides.model) options.model = overrides.model;
31
+ if (overrides.maxTurns !== undefined && overrides.maxTurns > 0) {
32
+ options.maxTurns = overrides.maxTurns;
33
+ }
34
+ if (overrides.maxBudgetUsd != null)
35
+ options.maxBudgetUsd = overrides.maxBudgetUsd;
36
+ if (overrides.resumeSessionId) options.resume = overrides.resumeSessionId;
37
+
38
+ if (overrides.allowedTools?.length) {
39
+ options.allowedTools = mergeAllowedTools(
40
+ baseOptions.allowedTools,
41
+ overrides.allowedTools
42
+ );
43
+ }
44
+ if (overrides.disallowedTools?.length) {
45
+ options.disallowedTools = mergeDisallowedTools(
46
+ baseOptions.disallowedTools,
47
+ overrides.disallowedTools
48
+ );
49
+ }
50
+
51
+ if (overrides.systemPrompt) {
52
+ if (
53
+ typeof baseOptions.systemPrompt === "object" &&
54
+ baseOptions.systemPrompt?.type === "preset"
55
+ ) {
56
+ const baseAppend = baseOptions.systemPrompt.append || "";
57
+ options.systemPrompt = {
58
+ type: "preset",
59
+ preset: "claude_code",
60
+ append: [baseAppend, overrides.systemPrompt].filter(Boolean).join("\n"),
61
+ };
62
+ } else if (typeof baseOptions.systemPrompt === "string") {
63
+ options.systemPrompt = baseOptions.systemPrompt;
64
+ options.extraArgs = {
65
+ ...options.extraArgs,
66
+ "append-system-prompt": overrides.systemPrompt,
67
+ };
68
+ } else {
69
+ options.systemPrompt = {
70
+ type: "preset",
71
+ preset: "claude_code",
72
+ append: overrides.systemPrompt,
73
+ };
74
+ }
75
+ }
76
+
77
+ const q = query({ prompt, options });
78
+ let result: SDKResultMessage | undefined;
79
+
80
+ for await (const message of q) {
81
+ if (message.type === "result") {
82
+ result = message as SDKResultMessage;
83
+ }
84
+ }
85
+
86
+ if (!result) {
87
+ throw new Error("No result message received from Claude Code");
88
+ }
89
+
90
+ return result;
91
+ }
92
+
93
+ function formatResult(result: SDKResultMessage) {
94
+ const payload = buildResultPayload(
95
+ result as Parameters<typeof buildResultPayload>[0]
96
+ );
97
+ return {
98
+ content: [
99
+ { type: "text" as const, text: JSON.stringify(payload, null, 2) },
100
+ ],
101
+ isError: result.is_error,
102
+ };
103
+ }
104
+
105
+ function formatError(error: unknown) {
106
+ return {
107
+ content: [
108
+ { type: "text" as const, text: `Error: ${formatErrorMessage(error)}` },
109
+ ],
110
+ isError: true,
111
+ };
112
+ }
113
+
114
+ export function registerQueryTools(
115
+ server: McpServer,
116
+ baseOptions: Options,
117
+ toolName: string,
118
+ toolDescription: string
119
+ ) {
120
+ const replyToolName = `${toolName}_reply`;
121
+
122
+ server.registerTool(toolName, {
123
+ description: toolDescription,
124
+ inputSchema: z.object({
125
+ prompt: z.string().describe("Task or question for Claude Code"),
126
+ cwd: z.string().optional().describe("Working directory (overrides CLAUDE_CWD)"),
127
+ model: z.string().optional().describe('Model override (e.g. "sonnet", "opus", "haiku")'),
128
+ allowedTools: z.array(z.string()).optional().describe("Tool whitelist override"),
129
+ disallowedTools: z.array(z.string()).optional().describe("Tool blacklist override"),
130
+ maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
131
+ maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
132
+ systemPrompt: z.string().optional().describe("Additional system prompt (appended to server default)"),
133
+ }),
134
+ }, async ({ prompt, cwd, model, allowedTools, disallowedTools, maxTurns, maxBudgetUsd, systemPrompt }) => {
135
+ try {
136
+ const result = await runQuery(prompt, {
137
+ cwd, model, allowedTools, disallowedTools, maxTurns, maxBudgetUsd, systemPrompt,
138
+ }, baseOptions);
139
+ return formatResult(result);
140
+ } catch (error) {
141
+ return formatError(error);
142
+ }
143
+ });
144
+
145
+ if (baseOptions.persistSession !== false) {
146
+ server.registerTool(replyToolName, {
147
+ description: [
148
+ `Continue a previous ${toolName} conversation by session ID.`,
149
+ "Use this for follow-up questions, iterative refinement,",
150
+ "or multi-step workflows that build on prior context.",
151
+ ].join(" "),
152
+ inputSchema: z.object({
153
+ session_id: z.string().describe(`Session ID from a prior ${toolName} response`),
154
+ prompt: z.string().describe("Follow-up instruction or question"),
155
+ cwd: z.string().optional().describe("Working directory override"),
156
+ model: z.string().optional().describe("Model override"),
157
+ maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
158
+ maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
159
+ }),
160
+ }, async ({ session_id, prompt, cwd, model, maxTurns, maxBudgetUsd }) => {
161
+ try {
162
+ const result = await runQuery(prompt, {
163
+ cwd, model, maxTurns, maxBudgetUsd, resumeSessionId: session_id,
164
+ }, baseOptions);
165
+ return formatResult(result);
166
+ } catch (error) {
167
+ return formatError(error);
168
+ }
169
+ });
170
+ }
171
+ }
package/src/types.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { Options } from "@anthropic-ai/claude-agent-sdk";
2
+
3
+ export interface InvocationOverrides {
4
+ cwd?: string;
5
+ model?: string;
6
+ allowedTools?: string[];
7
+ disallowedTools?: string[];
8
+ maxTurns?: number;
9
+ maxBudgetUsd?: number;
10
+ systemPrompt?: string;
11
+ resumeSessionId?: string;
12
+ }
13
+
14
+ export interface OptionCatalogEntry {
15
+ key: string;
16
+ envVar: string;
17
+ label: string;
18
+ hint: string;
19
+ example: string;
20
+ }
21
+
22
+ export type { Options };
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
+ }