@aipermission/mcp 0.2.38 → 0.2.39
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 +58 -16
- package/dist/cli-flags.js +81 -0
- package/dist/cli.js +37 -6
- package/dist/client-registry.js +281 -0
- package/dist/doctor.js +134 -0
- package/dist/init.js +235 -161
- package/dist/install-skill.js +66 -151
- package/dist/instructions.js +1 -0
- package/dist/private-file.js +36 -0
- package/dist/server.js +8 -4
- package/package.json +4 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ target/profile/action permission model as future connectors.
|
|
|
18
18
|
|
|
19
19
|
`@aipermission/mcp` is the official MCP bridge package. The unscoped `aipermission` npm package is only a placeholder that points users here.
|
|
20
20
|
|
|
21
|
+
The bridge requires Node.js 20+ and npm/npx in the AI client's child-process
|
|
22
|
+
environment. GUI clients and native Windows shells can use a different `PATH`
|
|
23
|
+
from your interactive terminal.
|
|
24
|
+
|
|
21
25
|
The package includes MCP Registry metadata:
|
|
22
26
|
|
|
23
27
|
- `mcpName` in `package.json`
|
|
@@ -26,14 +30,37 @@ The package includes MCP Registry metadata:
|
|
|
26
30
|
## Install
|
|
27
31
|
|
|
28
32
|
```bash
|
|
29
|
-
npx -y @aipermission/mcp
|
|
33
|
+
npx -y @aipermission/mcp setup \
|
|
30
34
|
--provider codex \
|
|
35
|
+
--scope user \
|
|
31
36
|
--name aipermission
|
|
32
37
|
```
|
|
33
38
|
|
|
34
|
-
The
|
|
39
|
+
The setup command prompts for your AIPermission API token, writes the MCP client
|
|
40
|
+
configuration, and installs the native operator skill for the selected client.
|
|
41
|
+
Generated runtime configs pin the exact package version that wrote them; re-run
|
|
42
|
+
setup when you intentionally upgrade a client. Use `init` when you only want the
|
|
43
|
+
MCP config, or `install-skill` when you only want the skill.
|
|
44
|
+
|
|
45
|
+
Check both paths without printing the bearer token:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx -y @aipermission/mcp doctor --client codex --scope user
|
|
49
|
+
```
|
|
35
50
|
|
|
36
|
-
The generated MCP config contains a bearer token. Keep it private. For
|
|
51
|
+
The generated MCP config contains a bearer token. Keep it private. For
|
|
52
|
+
project-local configs such as `.mcp.json`, `.cursor/mcp.json`, and
|
|
53
|
+
`.vscode/mcp.json`, setup refuses to write into files already tracked by Git
|
|
54
|
+
unless `--force` is passed. For untracked project-local configs, it locally
|
|
55
|
+
excludes and verifies four paths: the final config, crash-safe temporary-file
|
|
56
|
+
pattern, Windows staging-directory pattern, and update lock. Config and skill
|
|
57
|
+
destinations reject symbolic links or junctions rather than following them.
|
|
58
|
+
Use `init --print` and update a link-managed config through its owning tool.
|
|
59
|
+
Print mode does not read or print a bearer token: it emits `YOUR_TOKEN_HERE`,
|
|
60
|
+
writes no config or skill, and requires you to replace the placeholder through
|
|
61
|
+
the client's private environment or config mechanism. Install the skill
|
|
62
|
+
separately when needed. If a token config is committed or shared, revoke that
|
|
63
|
+
token in the AIPermission UI.
|
|
37
64
|
|
|
38
65
|
## Manual Config
|
|
39
66
|
|
|
@@ -42,7 +69,7 @@ The generated MCP config contains a bearer token. Keep it private. For project-l
|
|
|
42
69
|
"mcpServers": {
|
|
43
70
|
"aipermission": {
|
|
44
71
|
"command": "npx",
|
|
45
|
-
"args": ["-y", "@aipermission/mcp@0.2.
|
|
72
|
+
"args": ["-y", "@aipermission/mcp@0.2.39"],
|
|
46
73
|
"env": {
|
|
47
74
|
"NODE_ENV": "production",
|
|
48
75
|
"AIPERMISSION_API_URL": "http://localhost:3210",
|
|
@@ -53,6 +80,9 @@ The generated MCP config contains a bearer token. Keep it private. For project-l
|
|
|
53
80
|
}
|
|
54
81
|
```
|
|
55
82
|
|
|
83
|
+
Replace `VERSION` with the release you intend to pin. The setup command does
|
|
84
|
+
this automatically.
|
|
85
|
+
|
|
56
86
|
## Tools
|
|
57
87
|
|
|
58
88
|
- `list_connector_targets`
|
|
@@ -162,30 +192,42 @@ temporary paths, archive staging paths, or local upload contents.
|
|
|
162
192
|
|
|
163
193
|
## Operator Skill
|
|
164
194
|
|
|
165
|
-
Install the
|
|
195
|
+
Install only the AIPermission native operator skill for your AI client:
|
|
166
196
|
|
|
167
197
|
```bash
|
|
168
|
-
npx -y @aipermission/mcp install-skill --client codex
|
|
198
|
+
npx -y @aipermission/mcp install-skill --client codex --scope user
|
|
169
199
|
```
|
|
170
200
|
|
|
171
201
|
Supported clients:
|
|
172
202
|
|
|
173
|
-
- `codex`: `~/.
|
|
174
|
-
- `claude-code`: `.claude/
|
|
175
|
-
- `cursor`: `.cursor/
|
|
176
|
-
- `vscode`: `.github/
|
|
177
|
-
- `windsurf`: `.windsurf/
|
|
178
|
-
- `antigravity`: `.agents/
|
|
179
|
-
- `
|
|
180
|
-
- `
|
|
203
|
+
- `codex`: `~/.agents/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
|
|
204
|
+
- `claude-code`: `~/.claude/skills/.../SKILL.md` or `.claude/skills/.../SKILL.md`
|
|
205
|
+
- `cursor`: `~/.cursor/skills/.../SKILL.md` or `.cursor/skills/.../SKILL.md`
|
|
206
|
+
- `vscode` / `copilot`: `~/.copilot/skills/.../SKILL.md` or `.github/skills/.../SKILL.md`
|
|
207
|
+
- `windsurf`: `~/.codeium/windsurf/skills/.../SKILL.md` or `.windsurf/skills/.../SKILL.md`
|
|
208
|
+
- `antigravity`: `~/.gemini/config/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
|
|
209
|
+
- `antigravity-cli`: `~/.gemini/antigravity-cli/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
|
|
210
|
+
- `gemini`: `~/.gemini/skills/.../SKILL.md` or `.gemini/skills/.../SKILL.md`
|
|
211
|
+
- `grok`: `~/.grok/skills/.../SKILL.md` or `.grok/skills/.../SKILL.md`
|
|
212
|
+
- `agents`: `~/.agents/skills/.../SKILL.md` or `.agents/skills/.../SKILL.md`
|
|
213
|
+
- `custom`: prints the canonical skill Markdown to stdout
|
|
181
214
|
|
|
182
215
|
These instructions teach the agent how to discover connector targets, poll
|
|
183
216
|
`approval_pending` and `running` connector action requests, handle `stale`
|
|
184
217
|
approvals by sending a fresh request, avoid blind retries for `outcome_unknown`,
|
|
185
218
|
write short reasons, use explicit file transfer paths, and avoid printing
|
|
186
219
|
secrets. The default installer uses the
|
|
187
|
-
operator
|
|
188
|
-
|
|
220
|
+
operator skill bundled in the npm package; `--source` accepts local file paths
|
|
221
|
+
only and rejects HTTP(S) sources. The MCP server also publishes a concise
|
|
222
|
+
instruction summary during initialization for clients that surface protocol
|
|
223
|
+
instructions.
|
|
224
|
+
|
|
225
|
+
Client-specific home overrides are honored where the client documents them:
|
|
226
|
+
`CODEX_HOME` for Codex MCP config, `CLAUDE_CONFIG_DIR` for Claude user skills,
|
|
227
|
+
`COPILOT_HOME` for Copilot config and skills, `GEMINI_CLI_HOME` for Gemini
|
|
228
|
+
config and skills, and `GROK_HOME` for Grok config and skills. An explicit
|
|
229
|
+
`--home` remains the base home directory and takes precedence over those
|
|
230
|
+
environment overrides.
|
|
189
231
|
|
|
190
232
|
## Security Boundary
|
|
191
233
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const commonPathFlags = {
|
|
2
|
+
home: "value",
|
|
3
|
+
"project-dir": "value",
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const schemas = {
|
|
7
|
+
init: {
|
|
8
|
+
provider: "value",
|
|
9
|
+
scope: "value",
|
|
10
|
+
name: "value",
|
|
11
|
+
"api-url": "value",
|
|
12
|
+
"token-stdin": "boolean",
|
|
13
|
+
print: "boolean",
|
|
14
|
+
force: "boolean",
|
|
15
|
+
...commonPathFlags,
|
|
16
|
+
},
|
|
17
|
+
setup: {
|
|
18
|
+
provider: "value",
|
|
19
|
+
scope: "value",
|
|
20
|
+
"skill-scope": "value",
|
|
21
|
+
name: "value",
|
|
22
|
+
"api-url": "value",
|
|
23
|
+
"token-stdin": "boolean",
|
|
24
|
+
print: "boolean",
|
|
25
|
+
force: "boolean",
|
|
26
|
+
"skill-source": "value",
|
|
27
|
+
...commonPathFlags,
|
|
28
|
+
},
|
|
29
|
+
"install-skill": {
|
|
30
|
+
client: "value",
|
|
31
|
+
scope: "value",
|
|
32
|
+
source: "value",
|
|
33
|
+
...commonPathFlags,
|
|
34
|
+
},
|
|
35
|
+
doctor: {
|
|
36
|
+
client: "value",
|
|
37
|
+
provider: "value",
|
|
38
|
+
scope: "value",
|
|
39
|
+
"mcp-scope": "value",
|
|
40
|
+
"skill-scope": "value",
|
|
41
|
+
name: "value",
|
|
42
|
+
...commonPathFlags,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function parseCommandFlags(command, argv = []) {
|
|
47
|
+
const schema = schemas[command];
|
|
48
|
+
if (!schema) throw new Error(`Unknown command schema: ${command}`);
|
|
49
|
+
const result = {};
|
|
50
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
51
|
+
const argument = argv[index];
|
|
52
|
+
if (!argument.startsWith("--")) throw new Error(`${command} does not accept positional argument: ${argument}`);
|
|
53
|
+
const separator = argument.indexOf("=");
|
|
54
|
+
const rawKey = argument.slice(2, separator < 0 ? undefined : separator);
|
|
55
|
+
if (!rawKey) throw new Error(`${command} received an empty option name`);
|
|
56
|
+
if (rawKey === "token") throw new Error("--token is not supported; use the hidden prompt or --token-stdin");
|
|
57
|
+
const kind = schema[rawKey];
|
|
58
|
+
if (!kind) throw new Error(`Unknown ${command} option: --${rawKey}`);
|
|
59
|
+
const key = camelCase(rawKey);
|
|
60
|
+
if (Object.hasOwn(result, key)) throw new Error(`Duplicate ${command} option: --${rawKey}`);
|
|
61
|
+
const inlineValue = separator < 0 ? undefined : argument.slice(separator + 1);
|
|
62
|
+
if (kind === "boolean") {
|
|
63
|
+
result[key] = parseBooleanOption(rawKey, inlineValue);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const value = inlineValue === undefined ? argv[++index] : inlineValue;
|
|
67
|
+
if (!value || value.startsWith("--")) throw new Error(`--${rawKey} requires a non-empty value`);
|
|
68
|
+
result[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseBooleanOption(key, value) {
|
|
74
|
+
if (value === undefined || value === "true") return true;
|
|
75
|
+
if (value === "false") return false;
|
|
76
|
+
throw new Error(`--${key} accepts only true or false`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function camelCase(value) {
|
|
80
|
+
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
81
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { getClientCatalog } from "./client-registry.js";
|
|
4
|
+
|
|
3
5
|
const command = process.argv[2] || "serve";
|
|
4
6
|
|
|
5
|
-
if (command === "init") {
|
|
6
|
-
const { runInit } = await import("./init.js");
|
|
7
|
-
await runInit(process.argv.slice(3));
|
|
7
|
+
if (command === "init" || command === "setup") {
|
|
8
|
+
const { runInit, runSetup } = await import("./init.js");
|
|
9
|
+
await (command === "setup" ? runSetup : runInit)(process.argv.slice(3));
|
|
8
10
|
} else if (command === "install-skill") {
|
|
9
11
|
const { runInstallSkill } = await import("./install-skill.js");
|
|
10
12
|
await runInstallSkill(process.argv.slice(3));
|
|
13
|
+
} else if (command === "doctor") {
|
|
14
|
+
const { runDoctor } = await import("./doctor.js");
|
|
15
|
+
const result = await runDoctor(process.argv.slice(3));
|
|
16
|
+
if (!result.ok) process.exitCode = 1;
|
|
11
17
|
} else if (command === "serve" || command === "server" || command === "start") {
|
|
12
18
|
await import("./server.js");
|
|
13
19
|
} else if (command === "--help" || command === "-h" || command === "help") {
|
|
@@ -19,25 +25,50 @@ if (command === "init") {
|
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
function printHelp() {
|
|
28
|
+
const catalog = getClientCatalog();
|
|
29
|
+
const mcpClients = catalog.filter((client) => client.supportsMCP).map((client) => client.id);
|
|
30
|
+
const skillClients = catalog.filter((client) => client.supportsSkill).map((client) => client.id);
|
|
31
|
+
const doctorClients = catalog.filter((client) => client.supportsMCP && client.supportsSkill).map((client) => client.id);
|
|
22
32
|
console.log(`aipermission MCP
|
|
23
33
|
|
|
24
34
|
Usage:
|
|
25
35
|
aipermission-mcp Start the MCP stdio server
|
|
26
36
|
aipermission-mcp init Configure an AI client interactively
|
|
37
|
+
aipermission-mcp setup Configure MCP and install its native skill
|
|
27
38
|
aipermission-mcp install-skill Install the operator skill for an AI client
|
|
39
|
+
aipermission-mcp doctor Check MCP config and native skill paths
|
|
28
40
|
|
|
29
41
|
Init flags:
|
|
30
|
-
--provider
|
|
42
|
+
--provider ${[...mcpClients, "custom"].join("|")}
|
|
43
|
+
--scope user|project
|
|
31
44
|
--name aipermission
|
|
32
45
|
--token-stdin
|
|
33
46
|
--api-url http://localhost:3210
|
|
34
|
-
--print
|
|
47
|
+
--print Print a token-placeholder preview; change no files
|
|
48
|
+
--force
|
|
49
|
+
--home /path/to/home
|
|
50
|
+
--project-dir /path/to/workspace
|
|
51
|
+
|
|
52
|
+
Setup-only flags:
|
|
53
|
+
--skill-scope user|project
|
|
54
|
+
--skill-source /path/to/SKILL.md Local file only
|
|
35
55
|
|
|
36
56
|
Install skill flags:
|
|
37
|
-
--client
|
|
57
|
+
--client ${[...skillClients, "custom"].join("|")}
|
|
58
|
+
--scope user|project
|
|
59
|
+
--home /path/to/home
|
|
38
60
|
--project-dir /path/to/workspace
|
|
39
61
|
--source /path/to/SKILL.md Local file only; HTTP(S) sources are rejected
|
|
40
62
|
|
|
63
|
+
Doctor flags:
|
|
64
|
+
--client ${doctorClients.join("|")}
|
|
65
|
+
--scope user|project
|
|
66
|
+
--mcp-scope user|project
|
|
67
|
+
--skill-scope user|project
|
|
68
|
+
--name aipermission
|
|
69
|
+
--home /path/to/home
|
|
70
|
+
--project-dir /path/to/workspace
|
|
71
|
+
|
|
41
72
|
Security:
|
|
42
73
|
Use the hidden token prompt or --token-stdin. AIPERMISSION_API_URL must point
|
|
43
74
|
to localhost, 127.0.0.1, or [::1].
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const clients = [
|
|
5
|
+
client("codex", "OpenAI Codex", {
|
|
6
|
+
mcp: mcpConfig("user", {
|
|
7
|
+
user: tomlTarget("home", ".codex/config.toml", { homeEnv: "CODEX_HOME", envPath: "config.toml" }),
|
|
8
|
+
project: tomlTarget("project", ".codex/config.toml"),
|
|
9
|
+
}),
|
|
10
|
+
skill: skillConfig("user", {
|
|
11
|
+
user: skillTarget("home", ".agents/skills"),
|
|
12
|
+
project: skillTarget("project", ".agents/skills"),
|
|
13
|
+
}),
|
|
14
|
+
}),
|
|
15
|
+
client("claude-code", "Claude Code", {
|
|
16
|
+
aliases: ["claude", "claude_code"],
|
|
17
|
+
mcp: mcpConfig("project", {
|
|
18
|
+
user: jsonTarget("home", ".claude.json", "mcpServers"),
|
|
19
|
+
project: jsonTarget("project", ".mcp.json", "mcpServers"),
|
|
20
|
+
}),
|
|
21
|
+
skill: skillConfig("project", {
|
|
22
|
+
user: skillTarget("home", ".claude/skills", { homeEnv: "CLAUDE_CONFIG_DIR", envPath: "skills" }),
|
|
23
|
+
project: skillTarget("project", ".claude/skills"),
|
|
24
|
+
}),
|
|
25
|
+
}),
|
|
26
|
+
client("cursor", "Cursor", {
|
|
27
|
+
mcp: mcpConfig("project", {
|
|
28
|
+
user: jsonTarget("home", ".cursor/mcp.json", "mcpServers"),
|
|
29
|
+
project: jsonTarget("project", ".cursor/mcp.json", "mcpServers"),
|
|
30
|
+
}),
|
|
31
|
+
skill: skillConfig("project", {
|
|
32
|
+
user: skillTarget("home", ".cursor/skills"),
|
|
33
|
+
project: skillTarget("project", ".cursor/skills"),
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
client("vscode", "VS Code", {
|
|
37
|
+
aliases: ["vs-code"],
|
|
38
|
+
mcp: mcpConfig(
|
|
39
|
+
"project",
|
|
40
|
+
{ project: jsonTarget("project", ".vscode/mcp.json", "servers") },
|
|
41
|
+
{ printScopes: { user: virtualJSONTarget("servers") } },
|
|
42
|
+
),
|
|
43
|
+
skill: skillConfig("project", {
|
|
44
|
+
user: skillTarget("home", ".copilot/skills"),
|
|
45
|
+
project: skillTarget("project", ".github/skills"),
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
client("copilot", "GitHub Copilot CLI", {
|
|
49
|
+
aliases: ["copilot-cli", "github-copilot"],
|
|
50
|
+
mcp: mcpConfig(
|
|
51
|
+
"user",
|
|
52
|
+
{
|
|
53
|
+
user: jsonTarget("home", ".copilot/mcp-config.json", "mcpServers", {
|
|
54
|
+
homeEnv: "COPILOT_HOME",
|
|
55
|
+
envPath: "mcp-config.json",
|
|
56
|
+
}),
|
|
57
|
+
project: jsonTarget("project", ".mcp.json", "mcpServers"),
|
|
58
|
+
},
|
|
59
|
+
{ serverExtras: { type: "local", tools: ["*"] } },
|
|
60
|
+
),
|
|
61
|
+
skill: skillConfig("user", {
|
|
62
|
+
user: skillTarget("home", ".copilot/skills", { homeEnv: "COPILOT_HOME", envPath: "skills" }),
|
|
63
|
+
project: skillTarget("project", ".github/skills"),
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
client("windsurf", "Windsurf Cascade (legacy)", {
|
|
67
|
+
aliases: ["windsurf-legacy", "cascade"],
|
|
68
|
+
mcp: mcpConfig("user", { user: jsonTarget("home", ".codeium/windsurf/mcp_config.json", "mcpServers") }),
|
|
69
|
+
skill: skillConfig("user", {
|
|
70
|
+
user: skillTarget("home", ".codeium/windsurf/skills"),
|
|
71
|
+
project: skillTarget("project", ".windsurf/skills"),
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
client("antigravity", "Google Antigravity IDE", {
|
|
75
|
+
aliases: ["google-antigravity"],
|
|
76
|
+
mcp: mcpConfig("user", {
|
|
77
|
+
user: jsonTarget("home", ".gemini/config/mcp_config.json", "mcpServers"),
|
|
78
|
+
project: jsonTarget("project", ".agents/mcp_config.json", "mcpServers"),
|
|
79
|
+
}),
|
|
80
|
+
skill: skillConfig("user", {
|
|
81
|
+
user: skillTarget("home", ".gemini/config/skills"),
|
|
82
|
+
project: skillTarget("project", ".agents/skills"),
|
|
83
|
+
}),
|
|
84
|
+
}),
|
|
85
|
+
client("antigravity-cli", "Google Antigravity CLI", {
|
|
86
|
+
aliases: ["agy"],
|
|
87
|
+
mcp: mcpConfig("user", {
|
|
88
|
+
user: jsonTarget("home", ".gemini/antigravity-cli/mcp_config.json", "mcpServers"),
|
|
89
|
+
project: jsonTarget("project", ".agents/mcp_config.json", "mcpServers"),
|
|
90
|
+
}),
|
|
91
|
+
skill: skillConfig("user", {
|
|
92
|
+
user: skillTarget("home", ".gemini/antigravity-cli/skills"),
|
|
93
|
+
project: skillTarget("project", ".agents/skills"),
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
client("gemini", "Gemini CLI", {
|
|
97
|
+
aliases: ["gemini-cli"],
|
|
98
|
+
mcp: mcpConfig("user", {
|
|
99
|
+
user: jsonTarget("home", ".gemini/settings.json", "mcpServers", {
|
|
100
|
+
homeEnv: "GEMINI_CLI_HOME",
|
|
101
|
+
envPath: "settings.json",
|
|
102
|
+
}),
|
|
103
|
+
project: jsonTarget("project", ".gemini/settings.json", "mcpServers"),
|
|
104
|
+
}),
|
|
105
|
+
skill: skillConfig("user", {
|
|
106
|
+
user: skillTarget("home", ".gemini/skills", { homeEnv: "GEMINI_CLI_HOME", envPath: "skills" }),
|
|
107
|
+
project: skillTarget("project", ".gemini/skills"),
|
|
108
|
+
}),
|
|
109
|
+
}),
|
|
110
|
+
client("grok", "Grok Build CLI", {
|
|
111
|
+
aliases: ["grok-build", "grok-cli"],
|
|
112
|
+
mcp: mcpConfig(
|
|
113
|
+
"user",
|
|
114
|
+
{
|
|
115
|
+
user: tomlTarget("home", ".grok/config.toml", { homeEnv: "GROK_HOME", envPath: "config.toml" }),
|
|
116
|
+
project: tomlTarget("project", ".grok/config.toml"),
|
|
117
|
+
},
|
|
118
|
+
{ serverExtras: { startup_timeout_sec: 60 } },
|
|
119
|
+
),
|
|
120
|
+
skill: skillConfig("user", {
|
|
121
|
+
user: skillTarget("home", ".grok/skills", { homeEnv: "GROK_HOME", envPath: "skills" }),
|
|
122
|
+
project: skillTarget("project", ".grok/skills"),
|
|
123
|
+
}),
|
|
124
|
+
}),
|
|
125
|
+
client("agents", "Agents standard", {
|
|
126
|
+
aliases: ["agents-standard"],
|
|
127
|
+
skill: skillConfig("project", {
|
|
128
|
+
user: skillTarget("home", ".agents/skills"),
|
|
129
|
+
project: skillTarget("project", ".agents/skills"),
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
client("custom", "Custom / copy-paste"),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
export const MCP_PROVIDERS = clients
|
|
136
|
+
.filter((entry) => entry.mcp || entry.id === "custom")
|
|
137
|
+
.map((entry) => ({ id: entry.id, label: entry.label, description: providerDescription(entry) }));
|
|
138
|
+
|
|
139
|
+
export function getClient(value) {
|
|
140
|
+
const normalized = normalizeClientID(value);
|
|
141
|
+
return clients.find((entry) => entry.id === normalized);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getClientCatalog() {
|
|
145
|
+
return clients.map(({ id, label, aliases, mcp, skill }) => ({
|
|
146
|
+
id,
|
|
147
|
+
label,
|
|
148
|
+
aliases: [...aliases],
|
|
149
|
+
supportsMCP: Boolean(mcp),
|
|
150
|
+
supportsSkill: Boolean(skill),
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function adaptMCPServerConfig(clientValue, config) {
|
|
155
|
+
const clientEntry = getClient(clientValue);
|
|
156
|
+
return { ...config, ...(clientEntry.mcp?.serverExtras || {}) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveSkillTarget(clientValue, scopeValue, options = {}) {
|
|
160
|
+
return resolveTarget(clientValue, "skill", scopeValue, options, "skill");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveMCPConfigTarget(clientValue, scopeValue, options = {}) {
|
|
164
|
+
return resolveTarget(clientValue, "mcp", scopeValue, options, "MCP config");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resolveMCPPrintTarget(clientValue, scopeValue, options = {}) {
|
|
168
|
+
const clientEntry = getClient(clientValue);
|
|
169
|
+
if (!clientEntry.mcp) throw new Error(`${clientEntry.label} does not have an MCP config format.`);
|
|
170
|
+
const scope = normalizeScope(scopeValue || clientEntry.mcp.defaultScope);
|
|
171
|
+
const target = clientEntry.mcp.scopes[scope] || clientEntry.mcp.printScopes?.[scope];
|
|
172
|
+
if (!target) return resolveMCPConfigTarget(clientValue, scope, options);
|
|
173
|
+
return resolvedTarget(clientEntry, target, scope, options);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function normalizeClientID(value) {
|
|
177
|
+
const candidate = String(value || "")
|
|
178
|
+
.trim()
|
|
179
|
+
.toLowerCase();
|
|
180
|
+
const clientEntry = clients.find(
|
|
181
|
+
(entry) => entry.id === candidate || entry.label.toLowerCase() === candidate || entry.aliases.includes(candidate),
|
|
182
|
+
);
|
|
183
|
+
if (!clientEntry) throw new Error(`Unknown client: ${value}`);
|
|
184
|
+
return clientEntry.id;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function clientLabel(value) {
|
|
188
|
+
return getClient(value).label;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function normalizeScope(value) {
|
|
192
|
+
const scope = String(value || "")
|
|
193
|
+
.trim()
|
|
194
|
+
.toLowerCase();
|
|
195
|
+
if (scope !== "user" && scope !== "project") throw new Error(`Unknown scope: ${value}. Use user or project.`);
|
|
196
|
+
return scope;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveTarget(clientValue, capability, scopeValue, options, capabilityLabel) {
|
|
200
|
+
const clientEntry = getClient(clientValue);
|
|
201
|
+
const config = clientEntry[capability];
|
|
202
|
+
if (!config) throw new Error(`${clientEntry.label} does not have an automatic ${capabilityLabel} target.`);
|
|
203
|
+
const scope = normalizeScope(scopeValue || config.defaultScope);
|
|
204
|
+
const target = config.scopes[scope];
|
|
205
|
+
if (!target) {
|
|
206
|
+
const supported = Object.keys(config.scopes).join(", ");
|
|
207
|
+
throw new Error(`${clientEntry.label} does not support ${scope} ${capabilityLabel} scope. Supported scopes: ${supported}.`);
|
|
208
|
+
}
|
|
209
|
+
return resolvedTarget(clientEntry, target, scope, options);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolvedTarget(clientEntry, target, scope, options) {
|
|
213
|
+
const usesHomeOverride =
|
|
214
|
+
target.base === "home" && !options.homeDir && Boolean(target.homeEnv && (options.env || process.env)[target.homeEnv]);
|
|
215
|
+
const root = target.base === "project" ? path.resolve(options.projectDir || process.cwd()) : resolveHomeRoot(target, options);
|
|
216
|
+
return {
|
|
217
|
+
...target,
|
|
218
|
+
client: clientEntry.id,
|
|
219
|
+
label: clientEntry.label,
|
|
220
|
+
scope,
|
|
221
|
+
path: target.virtual ? "" : path.join(root, ...(usesHomeOverride ? target.envSegments : target.segments)),
|
|
222
|
+
trustedRoot: root,
|
|
223
|
+
projectConfig: target.base === "project",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveHomeRoot(target, options) {
|
|
228
|
+
if (options.homeDir) return path.resolve(options.homeDir);
|
|
229
|
+
const overridden = target.homeEnv && (options.env || process.env)[target.homeEnv];
|
|
230
|
+
return path.resolve(overridden || os.homedir());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function client(id, label, options = {}) {
|
|
234
|
+
return { id, label, aliases: options.aliases || [], mcp: options.mcp || null, skill: options.skill || null };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function mcpConfig(defaultScope, scopes, options = {}) {
|
|
238
|
+
return { defaultScope, scopes, printScopes: options.printScopes || {}, serverExtras: options.serverExtras || {} };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function skillConfig(defaultScope, scopes) {
|
|
242
|
+
return { defaultScope, scopes };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function jsonTarget(base, relativePath, rootKey, options = {}) {
|
|
246
|
+
return target(base, relativePath, { ...options, format: "json", rootKey });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function virtualJSONTarget(rootKey) {
|
|
250
|
+
return { base: "home", segments: [], envSegments: [], format: "json", rootKey, virtual: true };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function tomlTarget(base, relativePath, options = {}) {
|
|
254
|
+
return target(base, relativePath, { ...options, format: "toml", rootKey: "mcp_servers" });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function skillTarget(base, relativePath, options = {}) {
|
|
258
|
+
const suffix = "aipermission-operator/SKILL.md";
|
|
259
|
+
return target(base, `${relativePath}/${suffix}`, {
|
|
260
|
+
...options,
|
|
261
|
+
envPath: options.envPath ? `${options.envPath}/${suffix}` : undefined,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function target(base, relativePath, options = {}) {
|
|
266
|
+
return {
|
|
267
|
+
base,
|
|
268
|
+
segments: relativePath.split("/"),
|
|
269
|
+
homeEnv: options.homeEnv,
|
|
270
|
+
envSegments: (options.envPath || relativePath).split("/"),
|
|
271
|
+
format: options.format,
|
|
272
|
+
rootKey: options.rootKey,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function providerDescription(clientEntry) {
|
|
277
|
+
if (!clientEntry.mcp) return "Prints config snippets only";
|
|
278
|
+
const targetEntry = clientEntry.mcp.scopes[clientEntry.mcp.defaultScope];
|
|
279
|
+
const prefix = targetEntry.base === "home" ? "~/" : "./";
|
|
280
|
+
return `Writes ${prefix}${targetEntry.segments.join("/")} (${clientEntry.mcp.defaultScope})`;
|
|
281
|
+
}
|