claude-octopus 1.0.1 → 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 +65 -57
- 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 +2 -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 +17 -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 +2 -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 +18 -10
- package/src/tools/query.ts +57 -15
- package/src/types.ts +5 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/claude-octopus-icon.svg" alt="Claude Octopus" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Claude Octopus
|
|
2
6
|
|
|
3
7
|
One brain, many arms.
|
|
@@ -120,23 +124,27 @@ In factory-only mode, no query tools are registered — just the wizard. This ke
|
|
|
120
124
|
|
|
121
125
|
Each non-factory instance exposes:
|
|
122
126
|
|
|
123
|
-
| Tool
|
|
124
|
-
|
|
125
|
-
| `<name>`
|
|
126
|
-
| `<name>_reply` | Continue a previous conversation by `session_id`
|
|
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` |
|
|
127
131
|
|
|
128
132
|
Per-invocation parameters (override server defaults):
|
|
129
133
|
|
|
130
|
-
| Parameter
|
|
131
|
-
|
|
132
|
-
| `prompt`
|
|
133
|
-
| `cwd`
|
|
134
|
-
| `model`
|
|
135
|
-
| `
|
|
136
|
-
| `disallowedTools` |
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
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) |
|
|
140
148
|
|
|
141
149
|
## Configuration
|
|
142
150
|
|
|
@@ -144,50 +152,50 @@ All configuration is via environment variables in `.mcp.json`. Every env var is
|
|
|
144
152
|
|
|
145
153
|
### Identity
|
|
146
154
|
|
|
147
|
-
| Env Var
|
|
148
|
-
|
|
149
|
-
| `CLAUDE_TOOL_NAME`
|
|
150
|
-
| `CLAUDE_DESCRIPTION`
|
|
151
|
-
| `CLAUDE_SERVER_NAME`
|
|
152
|
-
| `CLAUDE_FACTORY_ONLY` | Only expose the factory wizard tool
|
|
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` |
|
|
153
161
|
|
|
154
162
|
### Agent
|
|
155
163
|
|
|
156
|
-
| Env Var
|
|
157
|
-
|
|
158
|
-
| `CLAUDE_MODEL`
|
|
159
|
-
| `CLAUDE_CWD`
|
|
160
|
-
| `CLAUDE_PERMISSION_MODE`
|
|
161
|
-
| `CLAUDE_ALLOWED_TOOLS`
|
|
162
|
-
| `CLAUDE_DISALLOWED_TOOLS` | Comma-separated tool blacklist
|
|
163
|
-
| `CLAUDE_MAX_TURNS`
|
|
164
|
-
| `CLAUDE_MAX_BUDGET_USD`
|
|
165
|
-
| `CLAUDE_EFFORT`
|
|
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 |
|
|
166
174
|
|
|
167
175
|
### Prompts
|
|
168
176
|
|
|
169
|
-
| Env Var
|
|
170
|
-
|
|
171
|
-
| `CLAUDE_SYSTEM_PROMPT` | Replaces the default Claude Code system prompt
|
|
177
|
+
| Env Var | Description |
|
|
178
|
+
| ---------------------- | ------------------------------------------------------ |
|
|
179
|
+
| `CLAUDE_SYSTEM_PROMPT` | Replaces the default Claude Code system prompt |
|
|
172
180
|
| `CLAUDE_APPEND_PROMPT` | Appended to the default prompt (usually what you want) |
|
|
173
181
|
|
|
174
182
|
### Advanced
|
|
175
183
|
|
|
176
|
-
| Env Var
|
|
177
|
-
|
|
178
|
-
| `CLAUDE_ADDITIONAL_DIRS` | Extra directories to grant access (comma-separated)
|
|
179
|
-
| `CLAUDE_PLUGINS`
|
|
180
|
-
| `CLAUDE_MCP_SERVERS`
|
|
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) |
|
|
181
189
|
| `CLAUDE_PERSIST_SESSION` | `true`/`false` — enable session resume (default: `true`) |
|
|
182
|
-
| `CLAUDE_SETTING_SOURCES` | Settings to load: `user`, `project`, `local`
|
|
183
|
-
| `CLAUDE_SETTINGS`
|
|
184
|
-
| `CLAUDE_BETAS`
|
|
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) |
|
|
185
193
|
|
|
186
194
|
### Authentication
|
|
187
195
|
|
|
188
|
-
| Env Var
|
|
189
|
-
|
|
190
|
-
| `ANTHROPIC_API_KEY`
|
|
196
|
+
| Env Var | Description | Default |
|
|
197
|
+
| ------------------------- | -------------------------------------- | --------------------- |
|
|
198
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key for this agent | inherited from parent |
|
|
191
199
|
| `CLAUDE_CODE_OAUTH_TOKEN` | Claude Code OAuth token for this agent | inherited from parent |
|
|
192
200
|
|
|
193
201
|
Leave both unset to inherit auth from the parent process. Set one per agent to use a different account or billing source.
|
|
@@ -196,10 +204,10 @@ Lists accept JSON arrays when values contain commas: `["path,with,comma", "/norm
|
|
|
196
204
|
|
|
197
205
|
## Security
|
|
198
206
|
|
|
199
|
-
- **Permission mode defaults to
|
|
200
|
-
- **`cwd` overrides
|
|
201
|
-
- **Tool restrictions narrow, never widen** — per-invocation `
|
|
202
|
-
- **`_reply
|
|
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`.
|
|
203
211
|
|
|
204
212
|
## Architecture
|
|
205
213
|
|
|
@@ -232,14 +240,14 @@ Lists accept JSON arrays when values contain commas: `["path,with,comma", "/norm
|
|
|
232
240
|
|
|
233
241
|
## How It Compares
|
|
234
242
|
|
|
235
|
-
| Feature
|
|
236
|
-
|
|
237
|
-
| Approach
|
|
238
|
-
| Exposes
|
|
239
|
-
| Multi-instance
|
|
240
|
-
| Per-instance config | No
|
|
241
|
-
| Factory wizard
|
|
242
|
-
| Session continuity
|
|
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 |
|
|
243
251
|
|
|
244
252
|
## Development
|
|
245
253
|
|
|
@@ -247,7 +255,7 @@ Lists accept JSON arrays when values contain commas: `["path,with,comma", "/norm
|
|
|
247
255
|
pnpm install
|
|
248
256
|
pnpm build # compile TypeScript
|
|
249
257
|
pnpm test # run tests (vitest)
|
|
250
|
-
pnpm test:coverage #
|
|
258
|
+
pnpm test:coverage # coverage report
|
|
251
259
|
```
|
|
252
260
|
|
|
253
261
|
## License
|
|
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
|
{
|
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.",
|
|
@@ -73,11 +73,17 @@ export function registerFactoryTool(server, serverEntry) {
|
|
|
73
73
|
Object.assign(env, optionEnv);
|
|
74
74
|
const configured = Object.keys(optionEnv);
|
|
75
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
|
+
}
|
|
76
82
|
const mcpEntry = {
|
|
77
83
|
[name]: {
|
|
78
|
-
command: "
|
|
79
|
-
args: [
|
|
80
|
-
env,
|
|
84
|
+
command: "npx",
|
|
85
|
+
args: ["claude-octopus"],
|
|
86
|
+
env: displayEnv,
|
|
81
87
|
},
|
|
82
88
|
};
|
|
83
89
|
const sections = [];
|
|
@@ -89,7 +95,8 @@ export function registerFactoryTool(server, serverEntry) {
|
|
|
89
95
|
for (const key of configured) {
|
|
90
96
|
const opt = OPTION_CATALOG.find((o) => o.envVar === key);
|
|
91
97
|
if (opt) {
|
|
92
|
-
|
|
98
|
+
const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
|
|
99
|
+
sections.push(`| ${opt.label} | \`${val}\` |`);
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
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
|
{
|
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.",
|
|
@@ -93,11 +92,19 @@ export function registerFactoryTool(
|
|
|
93
92
|
(o) => !configured.includes(o.envVar)
|
|
94
93
|
);
|
|
95
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
|
+
|
|
96
103
|
const mcpEntry = {
|
|
97
104
|
[name]: {
|
|
98
|
-
command: "
|
|
99
|
-
args: [
|
|
100
|
-
env,
|
|
105
|
+
command: "npx",
|
|
106
|
+
args: ["claude-octopus"],
|
|
107
|
+
env: displayEnv,
|
|
101
108
|
},
|
|
102
109
|
};
|
|
103
110
|
|
|
@@ -120,7 +127,8 @@ export function registerFactoryTool(
|
|
|
120
127
|
for (const key of configured) {
|
|
121
128
|
const opt = OPTION_CATALOG.find((o) => o.envVar === key);
|
|
122
129
|
if (opt) {
|
|
123
|
-
|
|
130
|
+
const val = SENSITIVE_KEYS.has(key) ? "<REDACTED>" : env[key];
|
|
131
|
+
sections.push(`| ${opt.label} | \`${val}\` |`);
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
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;
|