claude-octopus 1.0.0 → 1.0.2
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.
- package/README.md +263 -0
- package/assets/claude-octopus-icon.png +0 -0
- package/assets/claude-octopus-icon.svg +50 -0
- package/dist/config.js +9 -8
- package/dist/constants.js +16 -2
- package/dist/index.js +5 -7
- package/dist/lib.d.ts +7 -1
- package/dist/lib.js +21 -2
- package/dist/tools/factory.d.ts +1 -1
- package/dist/tools/factory.js +19 -10
- package/dist/tools/query.js +52 -11
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
- package/src/config.ts +8 -7
- package/src/constants.ts +16 -2
- package/src/index.ts +5 -7
- package/src/lib.test.ts +53 -6
- package/src/lib.ts +22 -2
- package/src/tools/factory.ts +20 -10
- package/src/tools/query.ts +57 -15
- package/src/types.ts +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/claude-octopus-icon.svg" alt="Claude Octopus" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Claude Octopus
|
|
6
|
+
|
|
7
|
+
One brain, many arms.
|
|
8
|
+
|
|
9
|
+
An MCP server that wraps the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code/sdk), letting you run multiple specialized Claude Code agents — each with its own model, tools, system prompt, and personality — from any MCP client.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Claude Code is powerful. But one instance does everything the same way. Sometimes you want a **strict code reviewer** that only reads files. A **test writer** that defaults to TDD. A **cheap quick helper** on Haiku. A **deep thinker** on Opus.
|
|
14
|
+
|
|
15
|
+
Claude Octopus lets you spin up as many of these as you need. Same binary, different configurations. Each one shows up as a separate tool in your MCP client.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- **Node.js** >= 18
|
|
20
|
+
- **Claude Code** — the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) is bundled as a dependency, but it spawns Claude Code under the hood, so you need a working `claude` CLI installation
|
|
21
|
+
- **Anthropic API key** (`ANTHROPIC_API_KEY` env var) or an active Claude Code OAuth session
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install claude-octopus
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or skip the install entirely — use `npx` directly in your `.mcp.json` (see Quick Start below).
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
Add to your `.mcp.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"claude": {
|
|
39
|
+
"command": "npx",
|
|
40
|
+
"args": ["claude-octopus"],
|
|
41
|
+
"env": {
|
|
42
|
+
"CLAUDE_PERMISSION_MODE": "bypassPermissions"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This gives you two tools: `claude_code` and `claude_code_reply`. That's it — you have Claude Code as a tool.
|
|
50
|
+
|
|
51
|
+
## Multiple Agents
|
|
52
|
+
|
|
53
|
+
The real power is running several instances with different configurations:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"code-reviewer": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["claude-octopus"],
|
|
61
|
+
"env": {
|
|
62
|
+
"CLAUDE_TOOL_NAME": "code_reviewer",
|
|
63
|
+
"CLAUDE_SERVER_NAME": "code-reviewer",
|
|
64
|
+
"CLAUDE_DESCRIPTION": "Strict code reviewer. Finds bugs and security issues. Read-only.",
|
|
65
|
+
"CLAUDE_MODEL": "opus",
|
|
66
|
+
"CLAUDE_ALLOWED_TOOLS": "Read,Grep,Glob",
|
|
67
|
+
"CLAUDE_APPEND_PROMPT": "You are a strict code reviewer. Report real bugs, not style preferences.",
|
|
68
|
+
"CLAUDE_EFFORT": "high"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"test-writer": {
|
|
72
|
+
"command": "npx",
|
|
73
|
+
"args": ["claude-octopus"],
|
|
74
|
+
"env": {
|
|
75
|
+
"CLAUDE_TOOL_NAME": "test_writer",
|
|
76
|
+
"CLAUDE_SERVER_NAME": "test-writer",
|
|
77
|
+
"CLAUDE_DESCRIPTION": "Writes thorough tests with edge case coverage.",
|
|
78
|
+
"CLAUDE_MODEL": "sonnet",
|
|
79
|
+
"CLAUDE_APPEND_PROMPT": "Write tests first. Cover edge cases. TDD."
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"quick-qa": {
|
|
83
|
+
"command": "npx",
|
|
84
|
+
"args": ["claude-octopus"],
|
|
85
|
+
"env": {
|
|
86
|
+
"CLAUDE_TOOL_NAME": "quick_qa",
|
|
87
|
+
"CLAUDE_SERVER_NAME": "quick-qa",
|
|
88
|
+
"CLAUDE_DESCRIPTION": "Fast answers to quick coding questions.",
|
|
89
|
+
"CLAUDE_MODEL": "haiku",
|
|
90
|
+
"CLAUDE_MAX_BUDGET_USD": "0.02",
|
|
91
|
+
"CLAUDE_EFFORT": "low"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Your MCP client now sees three distinct tools — `code_reviewer`, `test_writer`, `quick_qa` — each purpose-built.
|
|
99
|
+
|
|
100
|
+
## Agent Factory
|
|
101
|
+
|
|
102
|
+
Don't want to write configs by hand? Add a factory instance:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"agent-factory": {
|
|
108
|
+
"command": "npx",
|
|
109
|
+
"args": ["claude-octopus"],
|
|
110
|
+
"env": {
|
|
111
|
+
"CLAUDE_FACTORY_ONLY": "true",
|
|
112
|
+
"CLAUDE_SERVER_NAME": "agent-factory"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This exposes a single `create_claude_code_mcp` tool — an interactive wizard. Tell it what you want ("a strict code reviewer that only reads files") and it generates the `.mcp.json` entry for you, listing all available options you can customize.
|
|
120
|
+
|
|
121
|
+
In factory-only mode, no query tools are registered — just the wizard. This keeps routing clean: the factory creates agents, the agents do work.
|
|
122
|
+
|
|
123
|
+
## Tools
|
|
124
|
+
|
|
125
|
+
Each non-factory instance exposes:
|
|
126
|
+
|
|
127
|
+
| Tool | Purpose |
|
|
128
|
+
| -------------- | ------------------------------------------------------- |
|
|
129
|
+
| `<name>` | Send a task to the agent, get a response + `session_id` |
|
|
130
|
+
| `<name>_reply` | Continue a previous conversation by `session_id` |
|
|
131
|
+
|
|
132
|
+
Per-invocation parameters (override server defaults):
|
|
133
|
+
|
|
134
|
+
| Parameter | Description |
|
|
135
|
+
| ----------------- | ----------------------------------------------- |
|
|
136
|
+
| `prompt` | The task or question (required) |
|
|
137
|
+
| `cwd` | Working directory override |
|
|
138
|
+
| `model` | Model override |
|
|
139
|
+
| `tools` | Restrict available tools (intersects with server restriction) |
|
|
140
|
+
| `disallowedTools` | Block additional tools (unions with server blacklist) |
|
|
141
|
+
| `additionalDirs` | Extra directories the agent can access |
|
|
142
|
+
| `plugins` | Additional plugin paths to load |
|
|
143
|
+
| `effort` | Thinking effort (`low`, `medium`, `high`, `max`) |
|
|
144
|
+
| `permissionMode` | Permission mode (can only tighten, never loosen) |
|
|
145
|
+
| `maxTurns` | Max conversation turns |
|
|
146
|
+
| `maxBudgetUsd` | Max spend in USD |
|
|
147
|
+
| `systemPrompt` | Additional prompt (appended to server default) |
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
All configuration is via environment variables in `.mcp.json`. Every env var is optional.
|
|
152
|
+
|
|
153
|
+
### Identity
|
|
154
|
+
|
|
155
|
+
| Env Var | Description | Default |
|
|
156
|
+
| --------------------- | ---------------------------------------------- | ---------------- |
|
|
157
|
+
| `CLAUDE_TOOL_NAME` | Tool name prefix (`<name>` and `<name>_reply`) | `claude_code` |
|
|
158
|
+
| `CLAUDE_DESCRIPTION` | Tool description shown to the host AI | generic |
|
|
159
|
+
| `CLAUDE_SERVER_NAME` | MCP server name in protocol handshake | `claude-octopus` |
|
|
160
|
+
| `CLAUDE_FACTORY_ONLY` | Only expose the factory wizard tool | `false` |
|
|
161
|
+
|
|
162
|
+
### Agent
|
|
163
|
+
|
|
164
|
+
| Env Var | Description | Default |
|
|
165
|
+
| ------------------------- | ----------------------------------------------------- | --------------- |
|
|
166
|
+
| `CLAUDE_MODEL` | Model (`sonnet`, `opus`, `haiku`, or full ID) | SDK default |
|
|
167
|
+
| `CLAUDE_CWD` | Working directory | `process.cwd()` |
|
|
168
|
+
| `CLAUDE_PERMISSION_MODE` | `default`, `acceptEdits`, `bypassPermissions`, `plan` | `default` |
|
|
169
|
+
| `CLAUDE_ALLOWED_TOOLS` | Comma-separated tool restriction (available tools) | all |
|
|
170
|
+
| `CLAUDE_DISALLOWED_TOOLS` | Comma-separated tool blacklist | none |
|
|
171
|
+
| `CLAUDE_MAX_TURNS` | Max conversation turns | unlimited |
|
|
172
|
+
| `CLAUDE_MAX_BUDGET_USD` | Max spend per invocation | unlimited |
|
|
173
|
+
| `CLAUDE_EFFORT` | `low`, `medium`, `high`, `max` | SDK default |
|
|
174
|
+
|
|
175
|
+
### Prompts
|
|
176
|
+
|
|
177
|
+
| Env Var | Description |
|
|
178
|
+
| ---------------------- | ------------------------------------------------------ |
|
|
179
|
+
| `CLAUDE_SYSTEM_PROMPT` | Replaces the default Claude Code system prompt |
|
|
180
|
+
| `CLAUDE_APPEND_PROMPT` | Appended to the default prompt (usually what you want) |
|
|
181
|
+
|
|
182
|
+
### Advanced
|
|
183
|
+
|
|
184
|
+
| Env Var | Description |
|
|
185
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
186
|
+
| `CLAUDE_ADDITIONAL_DIRS` | Extra directories to grant access (comma-separated) |
|
|
187
|
+
| `CLAUDE_PLUGINS` | Local plugin paths (comma-separated) |
|
|
188
|
+
| `CLAUDE_MCP_SERVERS` | MCP servers for the inner agent (JSON) |
|
|
189
|
+
| `CLAUDE_PERSIST_SESSION` | `true`/`false` — enable session resume (default: `true`) |
|
|
190
|
+
| `CLAUDE_SETTING_SOURCES` | Settings to load: `user`, `project`, `local` |
|
|
191
|
+
| `CLAUDE_SETTINGS` | Path to settings JSON or inline JSON |
|
|
192
|
+
| `CLAUDE_BETAS` | Beta features (comma-separated) |
|
|
193
|
+
|
|
194
|
+
### Authentication
|
|
195
|
+
|
|
196
|
+
| Env Var | Description | Default |
|
|
197
|
+
| ------------------------- | -------------------------------------- | --------------------- |
|
|
198
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key for this agent | inherited from parent |
|
|
199
|
+
| `CLAUDE_CODE_OAUTH_TOKEN` | Claude Code OAuth token for this agent | inherited from parent |
|
|
200
|
+
|
|
201
|
+
Leave both unset to inherit auth from the parent process. Set one per agent to use a different account or billing source.
|
|
202
|
+
|
|
203
|
+
Lists accept JSON arrays when values contain commas: `["path,with,comma", "/normal"]`
|
|
204
|
+
|
|
205
|
+
## Security
|
|
206
|
+
|
|
207
|
+
- **Permission mode defaults to ****`default`** — tool executions prompt for approval unless you explicitly set `bypassPermissions`.
|
|
208
|
+
- **`cwd` overrides preserve agent knowledge** — when the host overrides `cwd`, the agent's configured base directory is automatically added to `additionalDirectories` so it retains access to its own context.
|
|
209
|
+
- **Tool restrictions narrow, never widen** — per-invocation `tools` intersects with the server restriction (can only remove tools, not add). `disallowedTools` unions (can only block more).
|
|
210
|
+
- **`_reply`**** tool respects persistence** — not registered when `CLAUDE_PERSIST_SESSION=false`.
|
|
211
|
+
|
|
212
|
+
## Architecture
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
┌─────────────────────────────────┐
|
|
216
|
+
│ MCP Client │
|
|
217
|
+
│ (Claude Desktop, Cursor, etc.) │
|
|
218
|
+
│ │
|
|
219
|
+
│ Sees: code_reviewer, │
|
|
220
|
+
│ test_writer, quick_qa │
|
|
221
|
+
└──────────┬──────────────────────┘
|
|
222
|
+
│ JSON-RPC / stdio
|
|
223
|
+
┌──────────▼──────────────────────┐
|
|
224
|
+
│ Claude Octopus (per instance) │
|
|
225
|
+
│ │
|
|
226
|
+
│ Env: CLAUDE_MODEL=opus │
|
|
227
|
+
│ CLAUDE_ALLOWED_TOOLS=... │
|
|
228
|
+
│ CLAUDE_APPEND_PROMPT=... │
|
|
229
|
+
│ │
|
|
230
|
+
│ Calls: Agent SDK query() │
|
|
231
|
+
└──────────┬──────────────────────┘
|
|
232
|
+
│ in-process
|
|
233
|
+
┌──────────▼──────────────────────┐
|
|
234
|
+
│ Claude Agent SDK │
|
|
235
|
+
│ Runs autonomously: reads files,│
|
|
236
|
+
│ writes code, runs commands │
|
|
237
|
+
│ Returns result + session_id │
|
|
238
|
+
└─────────────────────────────────┘
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## How It Compares
|
|
242
|
+
|
|
243
|
+
| Feature | `` | [claude-code-mcp](https://github.com/steipete/claude-code-mcp) | **Claude Octopus** |
|
|
244
|
+
| ------------------- | ------------ | -------------------------------------------------------------- | ------------------ |
|
|
245
|
+
| Approach | Built-in | CLI wrapping | Agent SDK |
|
|
246
|
+
| Exposes | 16 raw tools | 1 prompt tool | 1 prompt + reply |
|
|
247
|
+
| Multi-instance | No | No | Yes |
|
|
248
|
+
| Per-instance config | No | No | Yes (18 env vars) |
|
|
249
|
+
| Factory wizard | No | No | Yes |
|
|
250
|
+
| Session continuity | No | No | Yes |
|
|
251
|
+
|
|
252
|
+
## Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
pnpm install
|
|
256
|
+
pnpm build # compile TypeScript
|
|
257
|
+
pnpm test # run tests (vitest)
|
|
258
|
+
pnpm test:coverage # coverage report
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
[ISC](https://github.com/xiaolai/claude-octopus/blob/main/LICENSE) - Xiaolai Li
|
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" shape-rendering="crispEdges">
|
|
2
|
+
<rect x="7" y="6" width="18" height="1" fill="#C56F52"/>
|
|
3
|
+
<rect x="7" y="7" width="18" height="1" fill="#C56F52"/>
|
|
4
|
+
<rect x="7" y="8" width="18" height="1" fill="#C56F52"/>
|
|
5
|
+
<rect x="7" y="9" width="18" height="1" fill="#C56F52"/>
|
|
6
|
+
<rect x="7" y="10" width="4" height="1" fill="#C56F52"/>
|
|
7
|
+
<rect x="13" y="10" width="6" height="1" fill="#C56F52"/>
|
|
8
|
+
<rect x="21" y="10" width="4" height="1" fill="#C56F52"/>
|
|
9
|
+
<rect x="7" y="11" width="4" height="1" fill="#C56F52"/>
|
|
10
|
+
<rect x="13" y="11" width="6" height="1" fill="#C56F52"/>
|
|
11
|
+
<rect x="21" y="11" width="4" height="1" fill="#C56F52"/>
|
|
12
|
+
<rect x="5" y="12" width="22" height="1" fill="#C56F52"/>
|
|
13
|
+
<rect x="5" y="13" width="22" height="1" fill="#C56F52"/>
|
|
14
|
+
<rect x="3" y="14" width="26" height="1" fill="#C56F52"/>
|
|
15
|
+
<rect x="3" y="15" width="26" height="1" fill="#C56F52"/>
|
|
16
|
+
<rect x="3" y="16" width="26" height="1" fill="#C56F52"/>
|
|
17
|
+
<rect x="3" y="17" width="26" height="1" fill="#C56F52"/>
|
|
18
|
+
<rect x="3" y="18" width="2" height="1" fill="#C56F52"/>
|
|
19
|
+
<rect x="7" y="18" width="18" height="1" fill="#C56F52"/>
|
|
20
|
+
<rect x="27" y="18" width="2" height="1" fill="#C56F52"/>
|
|
21
|
+
<rect x="3" y="19" width="2" height="1" fill="#C56F52"/>
|
|
22
|
+
<rect x="7" y="19" width="18" height="1" fill="#C56F52"/>
|
|
23
|
+
<rect x="27" y="19" width="2" height="1" fill="#C56F52"/>
|
|
24
|
+
<rect x="3" y="20" width="2" height="1" fill="#C56F52"/>
|
|
25
|
+
<rect x="7" y="20" width="18" height="1" fill="#C56F52"/>
|
|
26
|
+
<rect x="27" y="20" width="2" height="1" fill="#C56F52"/>
|
|
27
|
+
<rect x="3" y="21" width="2" height="1" fill="#C56F52"/>
|
|
28
|
+
<rect x="7" y="21" width="18" height="1" fill="#C56F52"/>
|
|
29
|
+
<rect x="27" y="21" width="2" height="1" fill="#C56F52"/>
|
|
30
|
+
<rect x="7" y="22" width="2" height="1" fill="#C56F52"/>
|
|
31
|
+
<rect x="11" y="22" width="2" height="1" fill="#C56F52"/>
|
|
32
|
+
<rect x="15" y="22" width="2" height="1" fill="#C56F52"/>
|
|
33
|
+
<rect x="19" y="22" width="2" height="1" fill="#C56F52"/>
|
|
34
|
+
<rect x="23" y="22" width="2" height="1" fill="#C56F52"/>
|
|
35
|
+
<rect x="7" y="23" width="2" height="1" fill="#C56F52"/>
|
|
36
|
+
<rect x="11" y="23" width="2" height="1" fill="#C56F52"/>
|
|
37
|
+
<rect x="15" y="23" width="2" height="1" fill="#C56F52"/>
|
|
38
|
+
<rect x="19" y="23" width="2" height="1" fill="#C56F52"/>
|
|
39
|
+
<rect x="23" y="23" width="2" height="1" fill="#C56F52"/>
|
|
40
|
+
<rect x="7" y="24" width="2" height="1" fill="#C56F52"/>
|
|
41
|
+
<rect x="11" y="24" width="2" height="1" fill="#C56F52"/>
|
|
42
|
+
<rect x="15" y="24" width="2" height="1" fill="#C56F52"/>
|
|
43
|
+
<rect x="19" y="24" width="2" height="1" fill="#C56F52"/>
|
|
44
|
+
<rect x="23" y="24" width="2" height="1" fill="#C56F52"/>
|
|
45
|
+
<rect x="7" y="25" width="2" height="1" fill="#C56F52"/>
|
|
46
|
+
<rect x="11" y="25" width="2" height="1" fill="#C56F52"/>
|
|
47
|
+
<rect x="15" y="25" width="2" height="1" fill="#C56F52"/>
|
|
48
|
+
<rect x="19" y="25" width="2" height="1" fill="#C56F52"/>
|
|
49
|
+
<rect x="23" y="25" width="2" height="1" fill="#C56F52"/>
|
|
50
|
+
</svg>
|
package/dist/config.js
CHANGED
|
@@ -16,9 +16,9 @@ export function buildBaseOptions() {
|
|
|
16
16
|
const model = envStr("CLAUDE_MODEL");
|
|
17
17
|
if (model)
|
|
18
18
|
opts.model = model;
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
opts.
|
|
19
|
+
const tools = envList("CLAUDE_ALLOWED_TOOLS");
|
|
20
|
+
if (tools)
|
|
21
|
+
opts.tools = tools;
|
|
22
22
|
const disallowed = envList("CLAUDE_DISALLOWED_TOOLS");
|
|
23
23
|
if (disallowed)
|
|
24
24
|
opts.disallowedTools = disallowed;
|
|
@@ -62,16 +62,17 @@ export function buildBaseOptions() {
|
|
|
62
62
|
}
|
|
63
63
|
const settings = envStr("CLAUDE_SETTINGS");
|
|
64
64
|
if (settings) {
|
|
65
|
-
|
|
65
|
+
const trimmed = settings.trim();
|
|
66
|
+
if (trimmed.startsWith("{")) {
|
|
66
67
|
try {
|
|
67
|
-
opts.settings = JSON.parse(
|
|
68
|
+
opts.settings = JSON.parse(trimmed);
|
|
68
69
|
}
|
|
69
|
-
catch {
|
|
70
|
-
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(`claude-octopus: invalid CLAUDE_SETTINGS JSON: ${e instanceof Error ? e.message : e}`);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
74
|
-
opts.settings =
|
|
75
|
+
opts.settings = trimmed;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
const betas = envList("CLAUDE_BETAS");
|
package/dist/constants.js
CHANGED
|
@@ -23,8 +23,8 @@ export const OPTION_CATALOG = [
|
|
|
23
23
|
{
|
|
24
24
|
key: "allowedTools",
|
|
25
25
|
envVar: "CLAUDE_ALLOWED_TOOLS",
|
|
26
|
-
label: "
|
|
27
|
-
hint: "
|
|
26
|
+
label: "Available tools",
|
|
27
|
+
hint: "Restrict agent to only these tools (comma-separated)",
|
|
28
28
|
example: '"Read,Grep,Glob" for read-only; "Bash,Read,Write,Edit,Grep,Glob" for full access',
|
|
29
29
|
},
|
|
30
30
|
{
|
|
@@ -111,4 +111,18 @@ export const OPTION_CATALOG = [
|
|
|
111
111
|
hint: "Enable beta capabilities (comma-separated)",
|
|
112
112
|
example: '"context-1m-2025-08-07" for 1M context window',
|
|
113
113
|
},
|
|
114
|
+
{
|
|
115
|
+
key: "apiKey",
|
|
116
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
117
|
+
label: "API key",
|
|
118
|
+
hint: "Anthropic API key for this agent (overrides inherited auth)",
|
|
119
|
+
example: '"sk-ant-api03-..." — leave unset to inherit from parent',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "oauthToken",
|
|
123
|
+
envVar: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
124
|
+
label: "OAuth token",
|
|
125
|
+
hint: "Claude Code OAuth token for this agent (overrides inherited auth)",
|
|
126
|
+
example: '"sk-ant-oat01-..." — leave unset to inherit from parent',
|
|
127
|
+
},
|
|
114
128
|
];
|
package/dist/index.js
CHANGED
|
@@ -10,15 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
-
import {
|
|
14
|
-
import { dirname, resolve } from "node:path";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
15
14
|
import { envStr, envBool, sanitizeToolName } from "./lib.js";
|
|
16
15
|
import { buildBaseOptions } from "./config.js";
|
|
17
16
|
import { registerQueryTools } from "./tools/query.js";
|
|
18
17
|
import { registerFactoryTool } from "./tools/factory.js";
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const SERVER_ENTRY = resolve(__dirname, "index.js");
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { version: PKG_VERSION } = require("../package.json");
|
|
22
20
|
// ── Configuration ──────────────────────────────────────────────────
|
|
23
21
|
const BASE_OPTIONS = buildBaseOptions();
|
|
24
22
|
const TOOL_NAME = sanitizeToolName(envStr("CLAUDE_TOOL_NAME") || "claude_code");
|
|
@@ -33,12 +31,12 @@ const DEFAULT_DESCRIPTION = [
|
|
|
33
31
|
].join(" ");
|
|
34
32
|
const TOOL_DESCRIPTION = envStr("CLAUDE_DESCRIPTION") || DEFAULT_DESCRIPTION;
|
|
35
33
|
// ── Server ─────────────────────────────────────────────────────────
|
|
36
|
-
const server = new McpServer({ name: SERVER_NAME, version:
|
|
34
|
+
const server = new McpServer({ name: SERVER_NAME, version: PKG_VERSION });
|
|
37
35
|
if (!FACTORY_ONLY) {
|
|
38
36
|
registerQueryTools(server, BASE_OPTIONS, TOOL_NAME, TOOL_DESCRIPTION);
|
|
39
37
|
}
|
|
40
38
|
if (FACTORY_ONLY) {
|
|
41
|
-
registerFactoryTool(server
|
|
39
|
+
registerFactoryTool(server);
|
|
42
40
|
}
|
|
43
41
|
// ── Start ──────────────────────────────────────────────────────────
|
|
44
42
|
async function main() {
|
package/dist/lib.d.ts
CHANGED
|
@@ -9,10 +9,16 @@ export declare function envJson<T>(key: string, env?: Record<string, string | un
|
|
|
9
9
|
export declare const MAX_TOOL_NAME_LEN: number;
|
|
10
10
|
export declare function sanitizeToolName(raw: string): string;
|
|
11
11
|
export declare function isDescendantPath(requested: string, baseCwd: string): boolean;
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function mergeTools(serverList: string[] | undefined, callList: string[]): string[];
|
|
13
13
|
export declare function mergeDisallowedTools(serverList: string[] | undefined, callList: string[]): string[];
|
|
14
14
|
export declare const VALID_PERM_MODES: Set<string>;
|
|
15
15
|
export declare function validatePermissionMode(mode: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Narrow permission mode: returns the stricter of base and override.
|
|
18
|
+
* Callers can tighten permissions but never loosen them.
|
|
19
|
+
* Returns base unchanged if override is invalid or less strict.
|
|
20
|
+
*/
|
|
21
|
+
export declare function narrowPermissionMode(base: string, override: string): string;
|
|
16
22
|
export declare function deriveServerName(description: string): string;
|
|
17
23
|
export declare function deriveToolName(name: string): string;
|
|
18
24
|
export declare function serializeArrayEnv(val: unknown[]): string;
|
package/dist/lib.js
CHANGED
|
@@ -69,7 +69,7 @@ export function isDescendantPath(requested, baseCwd) {
|
|
|
69
69
|
return normalReq.startsWith(baseWithSep);
|
|
70
70
|
}
|
|
71
71
|
// ── Tool restriction merging ───────────────────────────────────────
|
|
72
|
-
export function
|
|
72
|
+
export function mergeTools(serverList, callList) {
|
|
73
73
|
if (serverList?.length) {
|
|
74
74
|
const serverSet = new Set(serverList);
|
|
75
75
|
return callList.filter((t) => serverSet.has(t));
|
|
@@ -87,11 +87,30 @@ export const VALID_PERM_MODES = new Set([
|
|
|
87
87
|
"bypassPermissions",
|
|
88
88
|
"plan",
|
|
89
89
|
"dontAsk",
|
|
90
|
-
"auto",
|
|
91
90
|
]);
|
|
92
91
|
export function validatePermissionMode(mode) {
|
|
93
92
|
return VALID_PERM_MODES.has(mode) ? mode : "default";
|
|
94
93
|
}
|
|
94
|
+
// Strictness order: most permissive → most restrictive
|
|
95
|
+
const PERM_STRICTNESS = {
|
|
96
|
+
bypassPermissions: 0,
|
|
97
|
+
acceptEdits: 1,
|
|
98
|
+
default: 2,
|
|
99
|
+
plan: 3,
|
|
100
|
+
dontAsk: 4,
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Narrow permission mode: returns the stricter of base and override.
|
|
104
|
+
* Callers can tighten permissions but never loosen them.
|
|
105
|
+
* Returns base unchanged if override is invalid or less strict.
|
|
106
|
+
*/
|
|
107
|
+
export function narrowPermissionMode(base, override) {
|
|
108
|
+
if (!VALID_PERM_MODES.has(override))
|
|
109
|
+
return base;
|
|
110
|
+
const baseLevel = PERM_STRICTNESS[base] ?? 2;
|
|
111
|
+
const overrideLevel = PERM_STRICTNESS[override] ?? 2;
|
|
112
|
+
return overrideLevel >= baseLevel ? override : base;
|
|
113
|
+
}
|
|
95
114
|
// ── Factory name derivation ────────────────────────────────────────
|
|
96
115
|
export function deriveServerName(description) {
|
|
97
116
|
const slug = description
|
package/dist/tools/factory.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
export declare function registerFactoryTool(server: McpServer
|
|
2
|
+
export declare function registerFactoryTool(server: McpServer): void;
|
package/dist/tools/factory.js
CHANGED
|
@@ -23,14 +23,14 @@ function buildEnvFromParams(params) {
|
|
|
23
23
|
}
|
|
24
24
|
return env;
|
|
25
25
|
}
|
|
26
|
-
export function registerFactoryTool(server
|
|
26
|
+
export function registerFactoryTool(server) {
|
|
27
27
|
server.registerTool("create_claude_code_mcp", {
|
|
28
28
|
description: [
|
|
29
|
-
"
|
|
30
|
-
"WHEN TO USE: user says '
|
|
31
|
-
"'
|
|
32
|
-
"'
|
|
33
|
-
"
|
|
29
|
+
"Generate a .mcp.json config entry for a new Claude Octopus MCP server instance.",
|
|
30
|
+
"WHEN TO USE: user says 'octopus agent', 'octopus mcp',",
|
|
31
|
+
"'new octopus', 'add octopus', 'create octopus',",
|
|
32
|
+
"'octopus instance', 'octopus config', 'octopus server',",
|
|
33
|
+
"or any phrase combining 'octopus' with agent/mcp/new/add/create/config/server/setup.",
|
|
34
34
|
"This is a wizard: only a description is required.",
|
|
35
35
|
"Returns a ready-to-use .mcp.json config and lists all customization options.",
|
|
36
36
|
"Call again with more parameters to refine.",
|
|
@@ -55,6 +55,8 @@ export function registerFactoryTool(server, serverEntry) {
|
|
|
55
55
|
settingSources: z.array(z.string()).optional(),
|
|
56
56
|
mcpServers: z.record(z.string(), z.unknown()).optional(),
|
|
57
57
|
betas: z.array(z.string()).optional(),
|
|
58
|
+
apiKey: z.string().optional().describe("Anthropic API key for this agent (leave unset to inherit)"),
|
|
59
|
+
oauthToken: z.string().optional().describe("Claude Code OAuth token for this agent (leave unset to inherit)"),
|
|
58
60
|
}),
|
|
59
61
|
}, async (params) => {
|
|
60
62
|
const { description, name: nameParam, toolName: toolNameParam } = params;
|
|
@@ -71,11 +73,17 @@ export function registerFactoryTool(server, serverEntry) {
|
|
|
71
73
|
Object.assign(env, optionEnv);
|
|
72
74
|
const configured = Object.keys(optionEnv);
|
|
73
75
|
const notConfigured = OPTION_CATALOG.filter((o) => !configured.includes(o.envVar));
|
|
76
|
+
const SENSITIVE_KEYS = new Set(["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]);
|
|
77
|
+
// Redact secrets in rendered output, keep real values in config
|
|
78
|
+
const displayEnv = {};
|
|
79
|
+
for (const [k, v] of Object.entries(env)) {
|
|
80
|
+
displayEnv[k] = SENSITIVE_KEYS.has(k) ? "<REDACTED>" : v;
|
|
81
|
+
}
|
|
74
82
|
const mcpEntry = {
|
|
75
83
|
[name]: {
|
|
76
|
-
command: "
|
|
77
|
-
args: [
|
|
78
|
-
env,
|
|
84
|
+
command: "npx",
|
|
85
|
+
args: ["claude-octopus"],
|
|
86
|
+
env: displayEnv,
|
|
79
87
|
},
|
|
80
88
|
};
|
|
81
89
|
const sections = [];
|
|
@@ -87,7 +95,8 @@ export function registerFactoryTool(server, serverEntry) {
|
|
|
87
95
|
for (const key of configured) {
|
|
88
96
|
const opt = OPTION_CATALOG.find((o) => o.envVar === key);
|
|
89
97
|
if (opt) {
|
|
90
|
-
|
|
98
|
+
const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
|
|
99
|
+
sections.push(`| ${opt.label} | \`${val}\` |`);
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
if (configured.length === 0) {
|
package/dist/tools/query.js
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
-
import {
|
|
4
|
+
import { mergeTools, mergeDisallowedTools, narrowPermissionMode, buildResultPayload, formatErrorMessage, } from "../lib.js";
|
|
5
5
|
async function runQuery(prompt, overrides, baseOptions) {
|
|
6
6
|
const options = { ...baseOptions };
|
|
7
|
+
// Handle cwd override — accept any path, preserve agent's base access
|
|
7
8
|
if (overrides.cwd) {
|
|
8
9
|
const baseCwd = baseOptions.cwd || process.cwd();
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const resolvedCwd = resolve(baseCwd, overrides.cwd);
|
|
11
|
+
if (resolvedCwd !== baseCwd) {
|
|
12
|
+
options.cwd = resolvedCwd;
|
|
13
|
+
// Agent's base dir becomes an additional dir so it keeps its knowledge
|
|
14
|
+
const dirs = new Set(options.additionalDirectories || []);
|
|
15
|
+
dirs.add(baseCwd);
|
|
16
|
+
options.additionalDirectories = [...dirs];
|
|
11
17
|
}
|
|
12
18
|
}
|
|
19
|
+
// Per-invocation additionalDirs — unions with server-level + auto-added dirs
|
|
20
|
+
if (overrides.additionalDirs?.length) {
|
|
21
|
+
const dirs = new Set(options.additionalDirectories || []);
|
|
22
|
+
for (const dir of overrides.additionalDirs) {
|
|
23
|
+
dirs.add(dir);
|
|
24
|
+
}
|
|
25
|
+
options.additionalDirectories = [...dirs];
|
|
26
|
+
}
|
|
27
|
+
// Per-invocation plugins — unions with server-level plugins
|
|
28
|
+
if (overrides.plugins?.length) {
|
|
29
|
+
const base = baseOptions.plugins || [];
|
|
30
|
+
const overridePaths = new Set(base.map((p) => p.path));
|
|
31
|
+
const merged = [...base];
|
|
32
|
+
for (const path of overrides.plugins) {
|
|
33
|
+
if (!overridePaths.has(path)) {
|
|
34
|
+
merged.push({ type: "local", path });
|
|
35
|
+
overridePaths.add(path);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
options.plugins = merged;
|
|
39
|
+
}
|
|
13
40
|
if (overrides.model)
|
|
14
41
|
options.model = overrides.model;
|
|
42
|
+
if (overrides.effort)
|
|
43
|
+
options.effort = overrides.effort;
|
|
44
|
+
// Permission mode can only tighten, never loosen
|
|
45
|
+
if (overrides.permissionMode) {
|
|
46
|
+
const base = baseOptions.permissionMode || "default";
|
|
47
|
+
const narrowed = narrowPermissionMode(base, overrides.permissionMode);
|
|
48
|
+
options.permissionMode = narrowed;
|
|
49
|
+
options.allowDangerouslySkipPermissions = narrowed === "bypassPermissions";
|
|
50
|
+
}
|
|
15
51
|
if (overrides.maxTurns !== undefined && overrides.maxTurns > 0) {
|
|
16
52
|
options.maxTurns = overrides.maxTurns;
|
|
17
53
|
}
|
|
@@ -19,8 +55,9 @@ async function runQuery(prompt, overrides, baseOptions) {
|
|
|
19
55
|
options.maxBudgetUsd = overrides.maxBudgetUsd;
|
|
20
56
|
if (overrides.resumeSessionId)
|
|
21
57
|
options.resume = overrides.resumeSessionId;
|
|
22
|
-
if (overrides.
|
|
23
|
-
|
|
58
|
+
if (overrides.tools?.length) {
|
|
59
|
+
const baseTools = Array.isArray(baseOptions.tools) ? baseOptions.tools : undefined;
|
|
60
|
+
options.tools = mergeTools(baseTools, overrides.tools);
|
|
24
61
|
}
|
|
25
62
|
if (overrides.disallowedTools?.length) {
|
|
26
63
|
options.disallowedTools = mergeDisallowedTools(baseOptions.disallowedTools, overrides.disallowedTools);
|
|
@@ -87,16 +124,20 @@ export function registerQueryTools(server, baseOptions, toolName, toolDescriptio
|
|
|
87
124
|
prompt: z.string().describe("Task or question for Claude Code"),
|
|
88
125
|
cwd: z.string().optional().describe("Working directory (overrides CLAUDE_CWD)"),
|
|
89
126
|
model: z.string().optional().describe('Model override (e.g. "sonnet", "opus", "haiku")'),
|
|
90
|
-
|
|
91
|
-
disallowedTools: z.array(z.string()).optional().describe("
|
|
127
|
+
tools: z.array(z.string()).optional().describe("Restrict available tools to this list (intersects with server-level restriction)"),
|
|
128
|
+
disallowedTools: z.array(z.string()).optional().describe("Additional tools to block (unions with server-level blacklist)"),
|
|
129
|
+
additionalDirs: z.array(z.string()).optional().describe("Extra directories the agent can access for this invocation"),
|
|
130
|
+
plugins: z.array(z.string()).optional().describe("Additional plugin paths to load for this invocation (unions with server-level plugins)"),
|
|
131
|
+
effort: z.enum(["low", "medium", "high", "max"]).optional().describe("Thinking effort override"),
|
|
132
|
+
permissionMode: z.enum(["default", "acceptEdits", "plan"]).optional().describe("Permission mode override (can only tighten, never loosen)"),
|
|
92
133
|
maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
|
|
93
|
-
maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
|
|
134
|
+
maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
|
|
94
135
|
systemPrompt: z.string().optional().describe("Additional system prompt (appended to server default)"),
|
|
95
136
|
}),
|
|
96
|
-
}, async ({ prompt, cwd, model,
|
|
137
|
+
}, async ({ prompt, cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt }) => {
|
|
97
138
|
try {
|
|
98
139
|
const result = await runQuery(prompt, {
|
|
99
|
-
cwd, model,
|
|
140
|
+
cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt,
|
|
100
141
|
}, baseOptions);
|
|
101
142
|
return formatResult(result);
|
|
102
143
|
}
|
|
@@ -117,7 +158,7 @@ export function registerQueryTools(server, baseOptions, toolName, toolDescriptio
|
|
|
117
158
|
cwd: z.string().optional().describe("Working directory override"),
|
|
118
159
|
model: z.string().optional().describe("Model override"),
|
|
119
160
|
maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
|
|
120
|
-
maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
|
|
161
|
+
maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
|
|
121
162
|
}),
|
|
122
163
|
}, async ({ session_id, prompt, cwd, model, maxTurns, maxBudgetUsd }) => {
|
|
123
164
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
|
2
2
|
export interface InvocationOverrides {
|
|
3
3
|
cwd?: string;
|
|
4
4
|
model?: string;
|
|
5
|
-
|
|
5
|
+
tools?: string[];
|
|
6
6
|
disallowedTools?: string[];
|
|
7
|
+
additionalDirs?: string[];
|
|
8
|
+
plugins?: string[];
|
|
9
|
+
effort?: string;
|
|
10
|
+
permissionMode?: string;
|
|
7
11
|
maxTurns?: number;
|
|
8
12
|
maxBudgetUsd?: number;
|
|
9
13
|
systemPrompt?: string;
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -29,8 +29,8 @@ export function buildBaseOptions(): Options {
|
|
|
29
29
|
const model = envStr("CLAUDE_MODEL");
|
|
30
30
|
if (model) opts.model = model;
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
32
|
+
const tools = envList("CLAUDE_ALLOWED_TOOLS");
|
|
33
|
+
if (tools) opts.tools = tools;
|
|
34
34
|
const disallowed = envList("CLAUDE_DISALLOWED_TOOLS");
|
|
35
35
|
if (disallowed) opts.disallowedTools = disallowed;
|
|
36
36
|
|
|
@@ -78,14 +78,15 @@ export function buildBaseOptions(): Options {
|
|
|
78
78
|
|
|
79
79
|
const settings = envStr("CLAUDE_SETTINGS");
|
|
80
80
|
if (settings) {
|
|
81
|
-
|
|
81
|
+
const trimmed = settings.trim();
|
|
82
|
+
if (trimmed.startsWith("{")) {
|
|
82
83
|
try {
|
|
83
|
-
opts.settings = JSON.parse(
|
|
84
|
-
} catch {
|
|
85
|
-
|
|
84
|
+
opts.settings = JSON.parse(trimmed);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(`claude-octopus: invalid CLAUDE_SETTINGS JSON: ${e instanceof Error ? e.message : e}`);
|
|
86
87
|
}
|
|
87
88
|
} else {
|
|
88
|
-
opts.settings =
|
|
89
|
+
opts.settings = trimmed;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
package/src/constants.ts
CHANGED
|
@@ -25,8 +25,8 @@ export const OPTION_CATALOG: OptionCatalogEntry[] = [
|
|
|
25
25
|
{
|
|
26
26
|
key: "allowedTools",
|
|
27
27
|
envVar: "CLAUDE_ALLOWED_TOOLS",
|
|
28
|
-
label: "
|
|
29
|
-
hint: "
|
|
28
|
+
label: "Available tools",
|
|
29
|
+
hint: "Restrict agent to only these tools (comma-separated)",
|
|
30
30
|
example: '"Read,Grep,Glob" for read-only; "Bash,Read,Write,Edit,Grep,Glob" for full access',
|
|
31
31
|
},
|
|
32
32
|
{
|
|
@@ -113,4 +113,18 @@ export const OPTION_CATALOG: OptionCatalogEntry[] = [
|
|
|
113
113
|
hint: "Enable beta capabilities (comma-separated)",
|
|
114
114
|
example: '"context-1m-2025-08-07" for 1M context window',
|
|
115
115
|
},
|
|
116
|
+
{
|
|
117
|
+
key: "apiKey",
|
|
118
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
119
|
+
label: "API key",
|
|
120
|
+
hint: "Anthropic API key for this agent (overrides inherited auth)",
|
|
121
|
+
example: '"sk-ant-api03-..." — leave unset to inherit from parent',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "oauthToken",
|
|
125
|
+
envVar: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
126
|
+
label: "OAuth token",
|
|
127
|
+
hint: "Claude Code OAuth token for this agent (overrides inherited auth)",
|
|
128
|
+
example: '"sk-ant-oat01-..." — leave unset to inherit from parent',
|
|
129
|
+
},
|
|
116
130
|
];
|
package/src/index.ts
CHANGED
|
@@ -12,16 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
-
import {
|
|
16
|
-
import { dirname, resolve } from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
17
16
|
import { envStr, envBool, sanitizeToolName } from "./lib.js";
|
|
18
17
|
import { buildBaseOptions } from "./config.js";
|
|
19
18
|
import { registerQueryTools } from "./tools/query.js";
|
|
20
19
|
import { registerFactoryTool } from "./tools/factory.js";
|
|
21
20
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const SERVER_ENTRY = resolve(__dirname, "index.js");
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const { version: PKG_VERSION } = require("../package.json");
|
|
25
23
|
|
|
26
24
|
// ── Configuration ──────────────────────────────────────────────────
|
|
27
25
|
|
|
@@ -43,14 +41,14 @@ const TOOL_DESCRIPTION = envStr("CLAUDE_DESCRIPTION") || DEFAULT_DESCRIPTION;
|
|
|
43
41
|
|
|
44
42
|
// ── Server ─────────────────────────────────────────────────────────
|
|
45
43
|
|
|
46
|
-
const server = new McpServer({ name: SERVER_NAME, version:
|
|
44
|
+
const server = new McpServer({ name: SERVER_NAME, version: PKG_VERSION });
|
|
47
45
|
|
|
48
46
|
if (!FACTORY_ONLY) {
|
|
49
47
|
registerQueryTools(server, BASE_OPTIONS, TOOL_NAME, TOOL_DESCRIPTION);
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
if (FACTORY_ONLY) {
|
|
53
|
-
registerFactoryTool(server
|
|
51
|
+
registerFactoryTool(server);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
// ── Start ──────────────────────────────────────────────────────────
|
package/src/lib.test.ts
CHANGED
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
sanitizeToolName,
|
|
9
9
|
MAX_TOOL_NAME_LEN,
|
|
10
10
|
isDescendantPath,
|
|
11
|
-
|
|
11
|
+
mergeTools,
|
|
12
12
|
mergeDisallowedTools,
|
|
13
13
|
validatePermissionMode,
|
|
14
|
+
narrowPermissionMode,
|
|
14
15
|
VALID_PERM_MODES,
|
|
15
16
|
deriveServerName,
|
|
16
17
|
deriveToolName,
|
|
@@ -202,24 +203,24 @@ describe("isDescendantPath", () => {
|
|
|
202
203
|
});
|
|
203
204
|
});
|
|
204
205
|
|
|
205
|
-
// ──
|
|
206
|
+
// ── mergeTools ───────────────────────────────────────────────────
|
|
206
207
|
|
|
207
|
-
describe("
|
|
208
|
+
describe("mergeTools", () => {
|
|
208
209
|
it("intersects when server has a list", () => {
|
|
209
210
|
expect(
|
|
210
|
-
|
|
211
|
+
mergeTools(["Read", "Grep", "Glob"], ["Read", "Write", "Glob"])
|
|
211
212
|
).toEqual(["Read", "Glob"]);
|
|
212
213
|
});
|
|
213
214
|
|
|
214
215
|
it("passes through when server has no list", () => {
|
|
215
216
|
expect(
|
|
216
|
-
|
|
217
|
+
mergeTools(undefined, ["Read", "Write"])
|
|
217
218
|
).toEqual(["Read", "Write"]);
|
|
218
219
|
});
|
|
219
220
|
|
|
220
221
|
it("returns empty when no overlap", () => {
|
|
221
222
|
expect(
|
|
222
|
-
|
|
223
|
+
mergeTools(["Read"], ["Write"])
|
|
223
224
|
).toEqual([]);
|
|
224
225
|
});
|
|
225
226
|
});
|
|
@@ -257,6 +258,52 @@ describe("validatePermissionMode", () => {
|
|
|
257
258
|
expect(validatePermissionMode("allowEdits")).toBe("default");
|
|
258
259
|
expect(validatePermissionMode("garbage")).toBe("default");
|
|
259
260
|
expect(validatePermissionMode("")).toBe("default");
|
|
261
|
+
expect(validatePermissionMode("auto")).toBe("default");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── narrowPermissionMode ──────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe("narrowPermissionMode", () => {
|
|
268
|
+
it("allows tightening from bypassPermissions to stricter modes", () => {
|
|
269
|
+
expect(narrowPermissionMode("bypassPermissions", "acceptEdits")).toBe("acceptEdits");
|
|
270
|
+
expect(narrowPermissionMode("bypassPermissions", "default")).toBe("default");
|
|
271
|
+
expect(narrowPermissionMode("bypassPermissions", "plan")).toBe("plan");
|
|
272
|
+
expect(narrowPermissionMode("bypassPermissions", "dontAsk")).toBe("dontAsk");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("allows tightening from acceptEdits to stricter modes", () => {
|
|
276
|
+
expect(narrowPermissionMode("acceptEdits", "default")).toBe("default");
|
|
277
|
+
expect(narrowPermissionMode("acceptEdits", "plan")).toBe("plan");
|
|
278
|
+
expect(narrowPermissionMode("acceptEdits", "dontAsk")).toBe("dontAsk");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("dontAsk is the strictest — rejects all loosening", () => {
|
|
282
|
+
expect(narrowPermissionMode("dontAsk", "bypassPermissions")).toBe("dontAsk");
|
|
283
|
+
expect(narrowPermissionMode("dontAsk", "acceptEdits")).toBe("dontAsk");
|
|
284
|
+
expect(narrowPermissionMode("dontAsk", "default")).toBe("dontAsk");
|
|
285
|
+
expect(narrowPermissionMode("dontAsk", "plan")).toBe("dontAsk");
|
|
286
|
+
expect(narrowPermissionMode("dontAsk", "dontAsk")).toBe("dontAsk");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("rejects loosening — returns base unchanged", () => {
|
|
290
|
+
expect(narrowPermissionMode("default", "bypassPermissions")).toBe("default");
|
|
291
|
+
expect(narrowPermissionMode("default", "acceptEdits")).toBe("default");
|
|
292
|
+
expect(narrowPermissionMode("plan", "bypassPermissions")).toBe("plan");
|
|
293
|
+
expect(narrowPermissionMode("plan", "acceptEdits")).toBe("plan");
|
|
294
|
+
expect(narrowPermissionMode("plan", "default")).toBe("plan");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("same mode returns same mode", () => {
|
|
298
|
+
expect(narrowPermissionMode("default", "default")).toBe("default");
|
|
299
|
+
expect(narrowPermissionMode("plan", "plan")).toBe("plan");
|
|
300
|
+
expect(narrowPermissionMode("bypassPermissions", "bypassPermissions")).toBe("bypassPermissions");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("invalid override returns base unchanged", () => {
|
|
304
|
+
expect(narrowPermissionMode("default", "garbage")).toBe("default");
|
|
305
|
+
expect(narrowPermissionMode("bypassPermissions", "")).toBe("bypassPermissions");
|
|
306
|
+
expect(narrowPermissionMode("default", "auto")).toBe("default");
|
|
260
307
|
});
|
|
261
308
|
});
|
|
262
309
|
|
package/src/lib.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function isDescendantPath(
|
|
|
94
94
|
|
|
95
95
|
// ── Tool restriction merging ───────────────────────────────────────
|
|
96
96
|
|
|
97
|
-
export function
|
|
97
|
+
export function mergeTools(
|
|
98
98
|
serverList: string[] | undefined,
|
|
99
99
|
callList: string[]
|
|
100
100
|
): string[] {
|
|
@@ -121,13 +121,33 @@ export const VALID_PERM_MODES = new Set([
|
|
|
121
121
|
"bypassPermissions",
|
|
122
122
|
"plan",
|
|
123
123
|
"dontAsk",
|
|
124
|
-
"auto",
|
|
125
124
|
]);
|
|
126
125
|
|
|
127
126
|
export function validatePermissionMode(mode: string): string {
|
|
128
127
|
return VALID_PERM_MODES.has(mode) ? mode : "default";
|
|
129
128
|
}
|
|
130
129
|
|
|
130
|
+
// Strictness order: most permissive → most restrictive
|
|
131
|
+
const PERM_STRICTNESS: Record<string, number> = {
|
|
132
|
+
bypassPermissions: 0,
|
|
133
|
+
acceptEdits: 1,
|
|
134
|
+
default: 2,
|
|
135
|
+
plan: 3,
|
|
136
|
+
dontAsk: 4,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Narrow permission mode: returns the stricter of base and override.
|
|
141
|
+
* Callers can tighten permissions but never loosen them.
|
|
142
|
+
* Returns base unchanged if override is invalid or less strict.
|
|
143
|
+
*/
|
|
144
|
+
export function narrowPermissionMode(base: string, override: string): string {
|
|
145
|
+
if (!VALID_PERM_MODES.has(override)) return base;
|
|
146
|
+
const baseLevel = PERM_STRICTNESS[base] ?? 2;
|
|
147
|
+
const overrideLevel = PERM_STRICTNESS[override] ?? 2;
|
|
148
|
+
return overrideLevel >= baseLevel ? override : base;
|
|
149
|
+
}
|
|
150
|
+
|
|
131
151
|
// ── Factory name derivation ────────────────────────────────────────
|
|
132
152
|
|
|
133
153
|
export function deriveServerName(description: string): string {
|
package/src/tools/factory.ts
CHANGED
|
@@ -33,15 +33,14 @@ function buildEnvFromParams(
|
|
|
33
33
|
|
|
34
34
|
export function registerFactoryTool(
|
|
35
35
|
server: McpServer,
|
|
36
|
-
serverEntry: string
|
|
37
36
|
) {
|
|
38
37
|
server.registerTool("create_claude_code_mcp", {
|
|
39
38
|
description: [
|
|
40
|
-
"
|
|
41
|
-
"WHEN TO USE: user says '
|
|
42
|
-
"'
|
|
43
|
-
"'
|
|
44
|
-
"
|
|
39
|
+
"Generate a .mcp.json config entry for a new Claude Octopus MCP server instance.",
|
|
40
|
+
"WHEN TO USE: user says 'octopus agent', 'octopus mcp',",
|
|
41
|
+
"'new octopus', 'add octopus', 'create octopus',",
|
|
42
|
+
"'octopus instance', 'octopus config', 'octopus server',",
|
|
43
|
+
"or any phrase combining 'octopus' with agent/mcp/new/add/create/config/server/setup.",
|
|
45
44
|
"This is a wizard: only a description is required.",
|
|
46
45
|
"Returns a ready-to-use .mcp.json config and lists all customization options.",
|
|
47
46
|
"Call again with more parameters to refine.",
|
|
@@ -68,6 +67,8 @@ export function registerFactoryTool(
|
|
|
68
67
|
settingSources: z.array(z.string()).optional(),
|
|
69
68
|
mcpServers: z.record(z.string(), z.unknown()).optional(),
|
|
70
69
|
betas: z.array(z.string()).optional(),
|
|
70
|
+
apiKey: z.string().optional().describe("Anthropic API key for this agent (leave unset to inherit)"),
|
|
71
|
+
oauthToken: z.string().optional().describe("Claude Code OAuth token for this agent (leave unset to inherit)"),
|
|
71
72
|
}),
|
|
72
73
|
}, async (params) => {
|
|
73
74
|
const { description, name: nameParam, toolName: toolNameParam } = params;
|
|
@@ -91,11 +92,19 @@ export function registerFactoryTool(
|
|
|
91
92
|
(o) => !configured.includes(o.envVar)
|
|
92
93
|
);
|
|
93
94
|
|
|
95
|
+
const SENSITIVE_KEYS = new Set(["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]);
|
|
96
|
+
|
|
97
|
+
// Redact secrets in rendered output, keep real values in config
|
|
98
|
+
const displayEnv: Record<string, string> = {};
|
|
99
|
+
for (const [k, v] of Object.entries(env)) {
|
|
100
|
+
displayEnv[k] = SENSITIVE_KEYS.has(k) ? "<REDACTED>" : v;
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
const mcpEntry = {
|
|
95
104
|
[name]: {
|
|
96
|
-
command: "
|
|
97
|
-
args: [
|
|
98
|
-
env,
|
|
105
|
+
command: "npx",
|
|
106
|
+
args: ["claude-octopus"],
|
|
107
|
+
env: displayEnv,
|
|
99
108
|
},
|
|
100
109
|
};
|
|
101
110
|
|
|
@@ -118,7 +127,8 @@ export function registerFactoryTool(
|
|
|
118
127
|
for (const key of configured) {
|
|
119
128
|
const opt = OPTION_CATALOG.find((o) => o.envVar === key);
|
|
120
129
|
if (opt) {
|
|
121
|
-
|
|
130
|
+
const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
|
|
131
|
+
sections.push(`| ${opt.label} | \`${val}\` |`);
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
if (configured.length === 0) {
|
package/src/tools/query.ts
CHANGED
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
8
8
|
import type { Options, InvocationOverrides } from "../types.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
mergeAllowedTools,
|
|
10
|
+
mergeTools,
|
|
12
11
|
mergeDisallowedTools,
|
|
12
|
+
narrowPermissionMode,
|
|
13
13
|
buildResultPayload,
|
|
14
14
|
formatErrorMessage,
|
|
15
15
|
} from "../lib.js";
|
|
@@ -21,13 +21,53 @@ async function runQuery(
|
|
|
21
21
|
): Promise<SDKResultMessage> {
|
|
22
22
|
const options: Options = { ...baseOptions };
|
|
23
23
|
|
|
24
|
+
// Handle cwd override — accept any path, preserve agent's base access
|
|
24
25
|
if (overrides.cwd) {
|
|
25
26
|
const baseCwd = baseOptions.cwd || process.cwd();
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const resolvedCwd = resolve(baseCwd, overrides.cwd);
|
|
28
|
+
if (resolvedCwd !== baseCwd) {
|
|
29
|
+
options.cwd = resolvedCwd;
|
|
30
|
+
// Agent's base dir becomes an additional dir so it keeps its knowledge
|
|
31
|
+
const dirs = new Set(options.additionalDirectories || []);
|
|
32
|
+
dirs.add(baseCwd);
|
|
33
|
+
options.additionalDirectories = [...dirs];
|
|
28
34
|
}
|
|
29
35
|
}
|
|
36
|
+
|
|
37
|
+
// Per-invocation additionalDirs — unions with server-level + auto-added dirs
|
|
38
|
+
if (overrides.additionalDirs?.length) {
|
|
39
|
+
const dirs = new Set(options.additionalDirectories || []);
|
|
40
|
+
for (const dir of overrides.additionalDirs) {
|
|
41
|
+
dirs.add(dir);
|
|
42
|
+
}
|
|
43
|
+
options.additionalDirectories = [...dirs];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Per-invocation plugins — unions with server-level plugins
|
|
47
|
+
if (overrides.plugins?.length) {
|
|
48
|
+
const base = baseOptions.plugins || [];
|
|
49
|
+
const overridePaths = new Set(base.map((p) => p.path));
|
|
50
|
+
const merged = [...base];
|
|
51
|
+
for (const path of overrides.plugins) {
|
|
52
|
+
if (!overridePaths.has(path)) {
|
|
53
|
+
merged.push({ type: "local" as const, path });
|
|
54
|
+
overridePaths.add(path);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
options.plugins = merged;
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
if (overrides.model) options.model = overrides.model;
|
|
61
|
+
if (overrides.effort) options.effort = overrides.effort as Options["effort"];
|
|
62
|
+
|
|
63
|
+
// Permission mode can only tighten, never loosen
|
|
64
|
+
if (overrides.permissionMode) {
|
|
65
|
+
const base = (baseOptions.permissionMode as string) || "default";
|
|
66
|
+
const narrowed = narrowPermissionMode(base, overrides.permissionMode);
|
|
67
|
+
options.permissionMode = narrowed as Options["permissionMode"];
|
|
68
|
+
options.allowDangerouslySkipPermissions = narrowed === "bypassPermissions";
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
if (overrides.maxTurns !== undefined && overrides.maxTurns > 0) {
|
|
32
72
|
options.maxTurns = overrides.maxTurns;
|
|
33
73
|
}
|
|
@@ -35,11 +75,9 @@ async function runQuery(
|
|
|
35
75
|
options.maxBudgetUsd = overrides.maxBudgetUsd;
|
|
36
76
|
if (overrides.resumeSessionId) options.resume = overrides.resumeSessionId;
|
|
37
77
|
|
|
38
|
-
if (overrides.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
overrides.allowedTools
|
|
42
|
-
);
|
|
78
|
+
if (overrides.tools?.length) {
|
|
79
|
+
const baseTools = Array.isArray(baseOptions.tools) ? baseOptions.tools : undefined;
|
|
80
|
+
options.tools = mergeTools(baseTools, overrides.tools);
|
|
43
81
|
}
|
|
44
82
|
if (overrides.disallowedTools?.length) {
|
|
45
83
|
options.disallowedTools = mergeDisallowedTools(
|
|
@@ -125,16 +163,20 @@ export function registerQueryTools(
|
|
|
125
163
|
prompt: z.string().describe("Task or question for Claude Code"),
|
|
126
164
|
cwd: z.string().optional().describe("Working directory (overrides CLAUDE_CWD)"),
|
|
127
165
|
model: z.string().optional().describe('Model override (e.g. "sonnet", "opus", "haiku")'),
|
|
128
|
-
|
|
129
|
-
disallowedTools: z.array(z.string()).optional().describe("
|
|
166
|
+
tools: z.array(z.string()).optional().describe("Restrict available tools to this list (intersects with server-level restriction)"),
|
|
167
|
+
disallowedTools: z.array(z.string()).optional().describe("Additional tools to block (unions with server-level blacklist)"),
|
|
168
|
+
additionalDirs: z.array(z.string()).optional().describe("Extra directories the agent can access for this invocation"),
|
|
169
|
+
plugins: z.array(z.string()).optional().describe("Additional plugin paths to load for this invocation (unions with server-level plugins)"),
|
|
170
|
+
effort: z.enum(["low", "medium", "high", "max"]).optional().describe("Thinking effort override"),
|
|
171
|
+
permissionMode: z.enum(["default", "acceptEdits", "plan"]).optional().describe("Permission mode override (can only tighten, never loosen)"),
|
|
130
172
|
maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
|
|
131
|
-
maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
|
|
173
|
+
maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
|
|
132
174
|
systemPrompt: z.string().optional().describe("Additional system prompt (appended to server default)"),
|
|
133
175
|
}),
|
|
134
|
-
}, async ({ prompt, cwd, model,
|
|
176
|
+
}, async ({ prompt, cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt }) => {
|
|
135
177
|
try {
|
|
136
178
|
const result = await runQuery(prompt, {
|
|
137
|
-
cwd, model,
|
|
179
|
+
cwd, model, tools, disallowedTools, additionalDirs, plugins, effort, permissionMode, maxTurns, maxBudgetUsd, systemPrompt,
|
|
138
180
|
}, baseOptions);
|
|
139
181
|
return formatResult(result);
|
|
140
182
|
} catch (error) {
|
|
@@ -155,7 +197,7 @@ export function registerQueryTools(
|
|
|
155
197
|
cwd: z.string().optional().describe("Working directory override"),
|
|
156
198
|
model: z.string().optional().describe("Model override"),
|
|
157
199
|
maxTurns: z.number().int().positive().optional().describe("Max conversation turns"),
|
|
158
|
-
maxBudgetUsd: z.number().optional().describe("Max spend in USD"),
|
|
200
|
+
maxBudgetUsd: z.number().positive().optional().describe("Max spend in USD"),
|
|
159
201
|
}),
|
|
160
202
|
}, async ({ session_id, prompt, cwd, model, maxTurns, maxBudgetUsd }) => {
|
|
161
203
|
try {
|
package/src/types.ts
CHANGED
|
@@ -3,8 +3,12 @@ import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
|
3
3
|
export interface InvocationOverrides {
|
|
4
4
|
cwd?: string;
|
|
5
5
|
model?: string;
|
|
6
|
-
|
|
6
|
+
tools?: string[];
|
|
7
7
|
disallowedTools?: string[];
|
|
8
|
+
additionalDirs?: string[];
|
|
9
|
+
plugins?: string[];
|
|
10
|
+
effort?: string;
|
|
11
|
+
permissionMode?: string;
|
|
8
12
|
maxTurns?: number;
|
|
9
13
|
maxBudgetUsd?: number;
|
|
10
14
|
systemPrompt?: string;
|