@aipermission/mcp 0.2.36 → 0.2.38
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 +2 -2
- package/dist/init.js +207 -95
- package/dist/install-skill.js +17 -16
- package/dist/private-file.js +299 -0
- package/dist/resources/aipermission-operator/SKILL.md +5 -2
- package/dist/server.js +34 -25
- package/package.json +17 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ npx -y @aipermission/mcp init \
|
|
|
33
33
|
|
|
34
34
|
The init command prompts for your AIPermission API token and writes the MCP client configuration for the selected provider. Generated runtime configs pin the exact package version that wrote them; re-run init when you intentionally upgrade a client.
|
|
35
35
|
|
|
36
|
-
The generated MCP config contains a bearer token. Keep it private. For project-local configs such as `.mcp.json`, `.cursor/mcp.json`, and `.vscode/mcp.json`, the init command refuses to write into files already tracked by Git unless `--force` is passed. For untracked project-local configs, it adds the file to `.git/info/exclude`
|
|
36
|
+
The generated MCP config contains a bearer token. Keep it private. For project-local configs such as `.mcp.json`, `.cursor/mcp.json`, and `.vscode/mcp.json`, the init command refuses to write into files already tracked by Git unless `--force` is passed. For untracked project-local configs, it adds both the final file and its crash-safe temporary-file pattern to `.git/info/exclude` before writing. Symbolic-link config destinations are rejected instead of being silently replaced; use `--print` and update a symlink-managed config through its owning tool. If a token config is committed or shared, revoke that token in the AIPermission UI.
|
|
37
37
|
|
|
38
38
|
## Manual Config
|
|
39
39
|
|
|
@@ -42,7 +42,7 @@ The generated MCP config contains a bearer token. Keep it private. For project-l
|
|
|
42
42
|
"mcpServers": {
|
|
43
43
|
"aipermission": {
|
|
44
44
|
"command": "npx",
|
|
45
|
-
"args": ["-y", "@aipermission/mcp@0.2.
|
|
45
|
+
"args": ["-y", "@aipermission/mcp@0.2.38"],
|
|
46
46
|
"env": {
|
|
47
47
|
"NODE_ENV": "production",
|
|
48
48
|
"AIPERMISSION_API_URL": "http://localhost:3210",
|
package/dist/init.js
CHANGED
|
@@ -8,6 +8,15 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { stdin as input, stdout as output } from "node:process";
|
|
10
10
|
import { DEFAULT_API_URL, normalizeLocalAPIURL } from "./local-url.js";
|
|
11
|
+
import {
|
|
12
|
+
atomicWritePrivateFile,
|
|
13
|
+
privateLockPath,
|
|
14
|
+
privateStagingIgnorePath,
|
|
15
|
+
privateStagingPath,
|
|
16
|
+
privateTemporaryIgnorePath,
|
|
17
|
+
privateTemporaryPath,
|
|
18
|
+
withPrivateFileLock,
|
|
19
|
+
} from "./private-file.js";
|
|
11
20
|
|
|
12
21
|
const require = createRequire(import.meta.url);
|
|
13
22
|
const execFileAsync = promisify(execFile);
|
|
@@ -78,9 +87,7 @@ export async function runInit(argv = []) {
|
|
|
78
87
|
const provider = flags.provider
|
|
79
88
|
? findProvider(flags.provider)
|
|
80
89
|
: await selectProvider("Which AI client should use this token?", providers);
|
|
81
|
-
const name = sanitizeName(
|
|
82
|
-
flags.name || (await ask(rl, "MCP server name", "aipermission"))
|
|
83
|
-
);
|
|
90
|
+
const name = sanitizeName(flags.name || (await ask(rl, "MCP server name", "aipermission")));
|
|
84
91
|
const apiUrl = normalizeURL(flags.apiUrl || DEFAULT_API_URL);
|
|
85
92
|
const token = await resolveToken({ ...flags, stdinToken }, rl);
|
|
86
93
|
|
|
@@ -104,7 +111,9 @@ export async function runInit(argv = []) {
|
|
|
104
111
|
console.log(`${color.dim}Git:${color.reset} added ${result.gitExcludeEntry} to .git/info/exclude`);
|
|
105
112
|
}
|
|
106
113
|
console.log("");
|
|
107
|
-
console.log(
|
|
114
|
+
console.log(
|
|
115
|
+
`${color.yellow}Keep this config private:${color.reset} it contains an AIPermission bearer token. If it is committed, revoke the token.`,
|
|
116
|
+
);
|
|
108
117
|
console.log(`${color.yellow}Restart the AI client so it reloads MCP servers.${color.reset}`);
|
|
109
118
|
} finally {
|
|
110
119
|
rl.close();
|
|
@@ -151,9 +160,7 @@ export function parseFlags(argv) {
|
|
|
151
160
|
|
|
152
161
|
function findProvider(idOrLabel) {
|
|
153
162
|
const normalized = String(idOrLabel).trim().toLowerCase();
|
|
154
|
-
const provider = providers.find(
|
|
155
|
-
(item) => item.id === normalized || item.label.toLowerCase() === normalized
|
|
156
|
-
);
|
|
163
|
+
const provider = providers.find((item) => item.id === normalized || item.label.toLowerCase() === normalized);
|
|
157
164
|
if (!provider) {
|
|
158
165
|
throw new Error(`Unknown provider: ${idOrLabel}`);
|
|
159
166
|
}
|
|
@@ -175,18 +182,12 @@ async function selectProvider(title, items) {
|
|
|
175
182
|
output.write(`\x1b[${renderedLines}A`);
|
|
176
183
|
output.write("\x1b[J");
|
|
177
184
|
}
|
|
178
|
-
const lines = [
|
|
179
|
-
`${color.bold}${color.cyan}${title}${color.reset}`,
|
|
180
|
-
`${color.dim}Use ↑/↓ and Enter.${color.reset}`,
|
|
181
|
-
"",
|
|
182
|
-
];
|
|
185
|
+
const lines = [`${color.bold}${color.cyan}${title}${color.reset}`, `${color.dim}Use ↑/↓ and Enter.${color.reset}`, ""];
|
|
183
186
|
for (let i = 0; i < items.length; i += 1) {
|
|
184
187
|
const selected = i === index;
|
|
185
188
|
const marker = selected ? `${color.green}›${color.reset}` : " ";
|
|
186
189
|
const label = selected ? `${color.bold}${items[i].label}${color.reset}` : items[i].label;
|
|
187
|
-
lines.push(
|
|
188
|
-
`${marker} ${label} ${color.dim}- ${items[i].description}${color.reset}`
|
|
189
|
-
);
|
|
190
|
+
lines.push(`${marker} ${label} ${color.dim}- ${items[i].description}${color.reset}`);
|
|
190
191
|
}
|
|
191
192
|
output.write("\x1b[?25l");
|
|
192
193
|
output.write(`${lines.join("\n")}\n`);
|
|
@@ -210,7 +211,7 @@ async function selectProvider(title, items) {
|
|
|
210
211
|
};
|
|
211
212
|
const onData = (buffer) => {
|
|
212
213
|
const value = buffer.toString("utf8");
|
|
213
|
-
const keys = value
|
|
214
|
+
const keys = splitInputKeys(value);
|
|
214
215
|
for (const key of keys) {
|
|
215
216
|
if (key === "\u0003") {
|
|
216
217
|
cleanup();
|
|
@@ -313,87 +314,112 @@ export function buildMCPServerConfig({ apiUrl, token }) {
|
|
|
313
314
|
}
|
|
314
315
|
|
|
315
316
|
export async function writeProviderConfig(providerID, name, config, options = {}) {
|
|
317
|
+
const projectRoot = process.cwd();
|
|
318
|
+
const homeRoot = os.homedir();
|
|
316
319
|
if (providerID === "codex") {
|
|
317
|
-
const filePath = path.join(
|
|
318
|
-
await writeCodexConfig(filePath, name, config);
|
|
320
|
+
const filePath = path.join(homeRoot, ".codex", "config.toml");
|
|
321
|
+
await writeCodexConfig(filePath, name, config, { trustedRoot: homeRoot });
|
|
319
322
|
return { path: filePath };
|
|
320
323
|
}
|
|
321
324
|
if (providerID === "claude-code") {
|
|
322
|
-
const filePath = path.join(
|
|
325
|
+
const filePath = path.join(projectRoot, ".mcp.json");
|
|
323
326
|
await assertProjectConfigWritable(filePath, options);
|
|
324
|
-
await
|
|
325
|
-
|
|
327
|
+
const protection = await protectGitIgnoredConfig(filePath);
|
|
328
|
+
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: projectRoot });
|
|
329
|
+
return { path: filePath, ...protection };
|
|
326
330
|
}
|
|
327
331
|
if (providerID === "cursor") {
|
|
328
|
-
const filePath = path.join(
|
|
332
|
+
const filePath = path.join(projectRoot, ".cursor", "mcp.json");
|
|
329
333
|
await assertProjectConfigWritable(filePath, options);
|
|
330
|
-
await
|
|
331
|
-
|
|
334
|
+
const protection = await protectGitIgnoredConfig(filePath);
|
|
335
|
+
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: projectRoot });
|
|
336
|
+
return { path: filePath, ...protection };
|
|
332
337
|
}
|
|
333
338
|
if (providerID === "vscode") {
|
|
334
|
-
const filePath = path.join(
|
|
339
|
+
const filePath = path.join(projectRoot, ".vscode", "mcp.json");
|
|
335
340
|
await assertProjectConfigWritable(filePath, options);
|
|
336
|
-
await
|
|
337
|
-
|
|
341
|
+
const protection = await protectGitIgnoredConfig(filePath);
|
|
342
|
+
await writeJSONMCPConfig(filePath, name, config, "servers", { trustedRoot: projectRoot });
|
|
343
|
+
return { path: filePath, ...protection };
|
|
338
344
|
}
|
|
339
345
|
if (providerID === "windsurf") {
|
|
340
|
-
const filePath = path.join(
|
|
341
|
-
await writeJSONMCPConfig(filePath, name, config, "mcpServers");
|
|
346
|
+
const filePath = path.join(homeRoot, ".codeium", "windsurf", "mcp_config.json");
|
|
347
|
+
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: homeRoot });
|
|
342
348
|
return { path: filePath };
|
|
343
349
|
}
|
|
344
350
|
if (providerID === "antigravity") {
|
|
345
|
-
const filePath = path.join(
|
|
346
|
-
await writeJSONMCPConfig(filePath, name, config, "mcpServers");
|
|
351
|
+
const filePath = path.join(homeRoot, ".gemini", "antigravity", "mcp_config.json");
|
|
352
|
+
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: homeRoot });
|
|
347
353
|
return { path: filePath };
|
|
348
354
|
}
|
|
349
355
|
if (providerID === "gemini") {
|
|
350
|
-
const filePath = path.join(
|
|
351
|
-
await writeJSONMCPConfig(filePath, name, config, "mcpServers");
|
|
356
|
+
const filePath = path.join(homeRoot, ".gemini", "settings.json");
|
|
357
|
+
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: homeRoot });
|
|
352
358
|
return { path: filePath };
|
|
353
359
|
}
|
|
354
360
|
throw new Error(`Unsupported provider: ${providerID}`);
|
|
355
361
|
}
|
|
356
362
|
|
|
357
|
-
export async function writeJSONMCPConfig(filePath, name, config, rootKey) {
|
|
358
|
-
await
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
363
|
+
export async function writeJSONMCPConfig(filePath, name, config, rootKey, options = {}) {
|
|
364
|
+
await withPrivateFileLock(
|
|
365
|
+
filePath,
|
|
366
|
+
async () => {
|
|
367
|
+
let root = {};
|
|
368
|
+
try {
|
|
369
|
+
root = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (error.code !== "ENOENT") {
|
|
372
|
+
throw new Error(`Could not read JSON config at ${filePath}: ${error.message}`, { cause: error });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
|
|
376
|
+
const currentServers = root[rootKey];
|
|
377
|
+
const servers =
|
|
378
|
+
currentServers && typeof currentServers === "object" && !Array.isArray(currentServers)
|
|
379
|
+
? { ...currentServers }
|
|
380
|
+
: Object.create(null);
|
|
381
|
+
Object.defineProperty(servers, name, { value: config, enumerable: true, configurable: true, writable: true });
|
|
382
|
+
root[rootKey] = servers;
|
|
383
|
+
await writePrivateFile(filePath, `${JSON.stringify(root, null, 2)}\n`, options);
|
|
384
|
+
},
|
|
385
|
+
options,
|
|
386
|
+
);
|
|
376
387
|
}
|
|
377
388
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
let
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
function splitInputKeys(value) {
|
|
390
|
+
const keys = [];
|
|
391
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
392
|
+
const sequence = value.slice(index, index + 3);
|
|
393
|
+
if (sequence === "\u001b[A" || sequence === "\u001b[B") {
|
|
394
|
+
keys.push(sequence);
|
|
395
|
+
index += 2;
|
|
396
|
+
continue;
|
|
386
397
|
}
|
|
398
|
+
keys.push(value[index]);
|
|
387
399
|
}
|
|
400
|
+
return keys;
|
|
401
|
+
}
|
|
388
402
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
403
|
+
async function writeCodexConfig(filePath, name, config, options = {}) {
|
|
404
|
+
await withPrivateFileLock(
|
|
405
|
+
filePath,
|
|
406
|
+
async () => {
|
|
407
|
+
let current = "";
|
|
408
|
+
try {
|
|
409
|
+
current = await fs.readFile(filePath, "utf8");
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error.code !== "ENOENT") throw error;
|
|
412
|
+
}
|
|
413
|
+
const next = removeCodexServer(current, name).trimEnd();
|
|
414
|
+
const block = codexServerBlock(name, config);
|
|
415
|
+
await writePrivateFile(filePath, `${next ? `${next}\n\n` : ""}${block}\n`, options);
|
|
416
|
+
},
|
|
417
|
+
options,
|
|
418
|
+
);
|
|
392
419
|
}
|
|
393
420
|
|
|
394
|
-
async function writePrivateFile(filePath, contents) {
|
|
395
|
-
await
|
|
396
|
-
await fs.chmod(filePath, 0o600);
|
|
421
|
+
async function writePrivateFile(filePath, contents, options = {}) {
|
|
422
|
+
await atomicWritePrivateFile(filePath, contents, options);
|
|
397
423
|
}
|
|
398
424
|
|
|
399
425
|
async function assertProjectConfigWritable(filePath, options = {}) {
|
|
@@ -408,7 +434,7 @@ async function assertProjectConfigWritable(filePath, options = {}) {
|
|
|
408
434
|
[
|
|
409
435
|
`Refusing to write AIPERMISSION_API_TOKEN into tracked git file: ${tracked}`,
|
|
410
436
|
"Use --print to copy the config manually, untrack/ignore that file, or rerun with --force if you intentionally accept commit risk.",
|
|
411
|
-
].join("\n")
|
|
437
|
+
].join("\n"),
|
|
412
438
|
);
|
|
413
439
|
}
|
|
414
440
|
|
|
@@ -421,26 +447,52 @@ async function protectGitIgnoredConfig(filePath) {
|
|
|
421
447
|
if (relativePath.startsWith("../") || path.isAbsolute(relativePath)) {
|
|
422
448
|
return {};
|
|
423
449
|
}
|
|
424
|
-
const excludePath =
|
|
425
|
-
|
|
450
|
+
const excludePath = repository.excludePath;
|
|
451
|
+
const temporaryRelativePath = path.relative(repository.workTree, privateTemporaryIgnorePath(filePath)).split(path.sep).join("/");
|
|
452
|
+
const stagingRelativePath = path.relative(repository.workTree, privateStagingIgnorePath(filePath)).split(path.sep).join("/");
|
|
453
|
+
const lockRelativePath = path.relative(repository.workTree, privateLockPath(filePath)).split(path.sep).join("/");
|
|
454
|
+
const ignoreEntries = [
|
|
455
|
+
gitIgnoreLiteral(relativePath),
|
|
456
|
+
gitIgnoreWildcardPath(temporaryRelativePath),
|
|
457
|
+
gitIgnoreWildcardPath(stagingRelativePath),
|
|
458
|
+
gitIgnoreLiteral(lockRelativePath),
|
|
459
|
+
];
|
|
426
460
|
try {
|
|
427
|
-
|
|
461
|
+
await withPrivateFileLock(
|
|
462
|
+
excludePath,
|
|
463
|
+
async () => {
|
|
464
|
+
let current = "";
|
|
465
|
+
try {
|
|
466
|
+
current = await fs.readFile(excludePath, "utf8");
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (error.code !== "ENOENT") throw error;
|
|
469
|
+
}
|
|
470
|
+
const entries = new Set(current.split(/\r?\n/));
|
|
471
|
+
const missingEntries = ignoreEntries.filter((entry) => !entries.has(entry));
|
|
472
|
+
if (missingEntries.length === 0) return;
|
|
473
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
474
|
+
await atomicWritePrivateFile(excludePath, `${current}${prefix}${missingEntries.join("\n")}\n`, {
|
|
475
|
+
trustedRoot: path.dirname(excludePath),
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
{ trustedRoot: path.dirname(excludePath) },
|
|
479
|
+
);
|
|
480
|
+
await assertGitIgnored(repository, [
|
|
481
|
+
relativePath,
|
|
482
|
+
privateTemporaryCheckPath(filePath, repository.workTree),
|
|
483
|
+
privateStagingCheckPath(filePath, repository.workTree),
|
|
484
|
+
lockRelativePath,
|
|
485
|
+
]);
|
|
428
486
|
} catch (error) {
|
|
429
|
-
|
|
430
|
-
return {};
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
const entries = current.split(/\r?\n/).map((line) => line.trim());
|
|
434
|
-
if (!entries.includes(relativePath)) {
|
|
435
|
-
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
436
|
-
try {
|
|
437
|
-
await fs.mkdir(path.dirname(excludePath), { recursive: true });
|
|
438
|
-
await fs.appendFile(excludePath, `${prefix}${relativePath}\n`, { mode: 0o600 });
|
|
439
|
-
} catch {
|
|
440
|
-
return {};
|
|
441
|
-
}
|
|
487
|
+
throw new Error(`Could not protect MCP config with local Git excludes: ${error.message}`, { cause: error });
|
|
442
488
|
}
|
|
443
|
-
return {
|
|
489
|
+
return {
|
|
490
|
+
gitExcluded: true,
|
|
491
|
+
gitExcludeEntry: relativePath,
|
|
492
|
+
gitExcludeTemporaryEntry: temporaryRelativePath,
|
|
493
|
+
gitExcludeStagingEntry: stagingRelativePath,
|
|
494
|
+
gitExcludeLockEntry: lockRelativePath,
|
|
495
|
+
};
|
|
444
496
|
}
|
|
445
497
|
|
|
446
498
|
async function gitTrackedPath(filePath) {
|
|
@@ -453,30 +505,87 @@ async function gitTrackedPath(filePath) {
|
|
|
453
505
|
return "";
|
|
454
506
|
}
|
|
455
507
|
try {
|
|
456
|
-
await execFileAsync("git", ["-C", repository.workTree, "ls-files", "--error-unmatch", "--", relativePath], {
|
|
508
|
+
await execFileAsync("git", ["-C", repository.workTree, "ls-files", "--error-unmatch", "--", relativePath], {
|
|
509
|
+
windowsHide: true,
|
|
510
|
+
});
|
|
457
511
|
return relativePath;
|
|
458
|
-
} catch {
|
|
459
|
-
return "";
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error.code === 1) return "";
|
|
514
|
+
throw new Error(`Could not verify whether MCP config is tracked by Git: ${gitErrorMessage(error)}`, { cause: error });
|
|
460
515
|
}
|
|
461
516
|
}
|
|
462
517
|
|
|
463
518
|
async function discoverGitRepository(startDir) {
|
|
464
519
|
try {
|
|
465
|
-
const { stdout } = await execFileAsync(
|
|
466
|
-
"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
);
|
|
520
|
+
const { stdout } = await execFileAsync("git", ["-C", path.resolve(startDir), "rev-parse", "--show-toplevel", "--absolute-git-dir"], {
|
|
521
|
+
encoding: "utf8",
|
|
522
|
+
windowsHide: true,
|
|
523
|
+
});
|
|
470
524
|
const [workTree, gitDir] = stdout.trim().split(/\r?\n/);
|
|
471
525
|
if (!workTree || !gitDir) {
|
|
472
526
|
return null;
|
|
473
527
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
528
|
+
const { stdout: excludeOutput } = await execFileAsync(
|
|
529
|
+
"git",
|
|
530
|
+
["-C", path.resolve(startDir), "rev-parse", "--path-format=absolute", "--git-path", "info/exclude"],
|
|
531
|
+
{ encoding: "utf8", windowsHide: true },
|
|
532
|
+
);
|
|
533
|
+
const excludePath = excludeOutput.trim();
|
|
534
|
+
if (!excludePath) throw new Error("Git did not return an exclude path");
|
|
535
|
+
return { workTree: path.resolve(workTree), gitDir: path.resolve(gitDir), excludePath: path.resolve(excludePath) };
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (/not a git repository/i.test(`${error.stderr || ""}\n${error.message || ""}`)) return null;
|
|
538
|
+
throw new Error(`Could not inspect Git repository: ${gitErrorMessage(error)}`, { cause: error });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function assertGitIgnored(repository, relativePaths) {
|
|
543
|
+
for (const relativePath of relativePaths) {
|
|
544
|
+
try {
|
|
545
|
+
await execFileAsync("git", ["-C", repository.workTree, "check-ignore", "--no-index", "-q", "--", relativePath], {
|
|
546
|
+
windowsHide: true,
|
|
547
|
+
});
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error.code === 1) {
|
|
550
|
+
throw new Error(`Git still permits sensitive MCP path: ${relativePath}`, { cause: error });
|
|
551
|
+
}
|
|
552
|
+
throw new Error(`Could not verify local Git exclusion: ${gitErrorMessage(error)}`, { cause: error });
|
|
553
|
+
}
|
|
477
554
|
}
|
|
478
555
|
}
|
|
479
556
|
|
|
557
|
+
function privateTemporaryCheckPath(filePath, workTree) {
|
|
558
|
+
return path.relative(workTree, privateTemporaryPath(filePath, "git-check")).split(path.sep).join("/");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function privateStagingCheckPath(filePath, workTree) {
|
|
562
|
+
return path.relative(workTree, privateStagingPath(filePath, "git-check")).split(path.sep).join("/");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function escapeGitIgnoreFragment(value) {
|
|
566
|
+
let result = "";
|
|
567
|
+
for (const character of value) {
|
|
568
|
+
result += ["\\", "*", "?", "[", "]", "#", "!", " "].includes(character) ? `\\${character}` : character;
|
|
569
|
+
}
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function gitIgnoreLiteral(relativePath) {
|
|
574
|
+
return `/${escapeGitIgnoreFragment(relativePath)}`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function gitIgnoreWildcardPath(relativePath) {
|
|
578
|
+
const wildcardIndex = relativePath.lastIndexOf("*");
|
|
579
|
+
if (wildcardIndex < 0) throw new Error(`Git ignore wildcard path is missing its generated wildcard: ${relativePath}`);
|
|
580
|
+
return `/${escapeGitIgnoreFragment(relativePath.slice(0, wildcardIndex))}*${escapeGitIgnoreFragment(
|
|
581
|
+
relativePath.slice(wildcardIndex + 1),
|
|
582
|
+
)}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function gitErrorMessage(error) {
|
|
586
|
+
return String(error.stderr || error.message || error).trim();
|
|
587
|
+
}
|
|
588
|
+
|
|
480
589
|
function removeCodexServer(source, name) {
|
|
481
590
|
const main = `[mcp_servers.${tomlKey(name)}]`;
|
|
482
591
|
const env = `[mcp_servers.${tomlKey(name)}.env]`;
|
|
@@ -533,6 +642,9 @@ export function sanitizeName(value) {
|
|
|
533
642
|
if (!name) {
|
|
534
643
|
throw new Error("MCP server name is required.");
|
|
535
644
|
}
|
|
645
|
+
if (["__proto__", "prototype", "constructor"].includes(name.toLowerCase())) {
|
|
646
|
+
throw new Error("MCP server name is reserved.");
|
|
647
|
+
}
|
|
536
648
|
return name;
|
|
537
649
|
}
|
|
538
650
|
|
package/dist/install-skill.js
CHANGED
|
@@ -79,10 +79,7 @@ export async function loadSkill(source) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function bundledSkillCandidates() {
|
|
82
|
-
return [
|
|
83
|
-
path.join(moduleDir, "resources", SKILL_NAME, "SKILL.md"),
|
|
84
|
-
path.join(moduleDir, "..", "resources", SKILL_NAME, "SKILL.md"),
|
|
85
|
-
];
|
|
82
|
+
return [path.join(moduleDir, "resources", SKILL_NAME, "SKILL.md"), path.join(moduleDir, "..", "resources", SKILL_NAME, "SKILL.md")];
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
async function readSkillSource(source) {
|
|
@@ -131,10 +128,12 @@ export function renderInstruction(client, skill) {
|
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
export function normalizeClient(value) {
|
|
134
|
-
const client = String(value || "")
|
|
131
|
+
const client = String(value || "")
|
|
132
|
+
.trim()
|
|
133
|
+
.toLowerCase();
|
|
135
134
|
const aliases = {
|
|
136
135
|
claude: "claude-code",
|
|
137
|
-
|
|
136
|
+
claude_code: "claude-code",
|
|
138
137
|
"claude-code": "claude-code",
|
|
139
138
|
copilot: "vscode",
|
|
140
139
|
"vs-code": "vscode",
|
|
@@ -151,16 +150,18 @@ export function normalizeClient(value) {
|
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
function clientLabel(client) {
|
|
154
|
-
return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
return (
|
|
154
|
+
{
|
|
155
|
+
codex: "Codex",
|
|
156
|
+
"claude-code": "Claude Code",
|
|
157
|
+
cursor: "Cursor",
|
|
158
|
+
vscode: "VS Code / GitHub Copilot",
|
|
159
|
+
windsurf: "Windsurf",
|
|
160
|
+
antigravity: "Google Antigravity",
|
|
161
|
+
gemini: "Gemini CLI",
|
|
162
|
+
custom: "Custom",
|
|
163
|
+
}[client] || client
|
|
164
|
+
);
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
function stripSkillFrontmatter(value) {
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const staleTemporaryAgeMs = 24 * 60 * 60 * 1000;
|
|
9
|
+
const lockRetryDelayMs = 50;
|
|
10
|
+
const lockRetryLimit = 100;
|
|
11
|
+
|
|
12
|
+
export async function atomicWritePrivateFile(filePath, contents, options = {}) {
|
|
13
|
+
const destination = path.resolve(filePath);
|
|
14
|
+
const rename = options.rename || fs.rename;
|
|
15
|
+
const suffix = options.suffix || `${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
16
|
+
const directory = path.dirname(destination);
|
|
17
|
+
let temporaryPath = privateTemporaryPath(destination, suffix);
|
|
18
|
+
let stagingDirectory;
|
|
19
|
+
let handle;
|
|
20
|
+
|
|
21
|
+
await ensurePrivateDirectory(directory, options);
|
|
22
|
+
await validatePrivateDestination(destination, options);
|
|
23
|
+
await cleanupStalePrivateFiles(destination, options);
|
|
24
|
+
try {
|
|
25
|
+
if (operatingSystem(options) === "win32") {
|
|
26
|
+
stagingDirectory = await createPrivateStagingDirectory(destination, options);
|
|
27
|
+
temporaryPath = path.join(stagingDirectory, path.basename(temporaryPath));
|
|
28
|
+
}
|
|
29
|
+
handle = await fs.open(temporaryPath, "wx", 0o600);
|
|
30
|
+
await enforcePrivateFilePermissions(temporaryPath, options);
|
|
31
|
+
await handle.writeFile(contents, { encoding: "utf8" });
|
|
32
|
+
await handle.sync();
|
|
33
|
+
await handle.close();
|
|
34
|
+
handle = undefined;
|
|
35
|
+
await validatePrivateDestination(destination, options);
|
|
36
|
+
await rename(temporaryPath, destination);
|
|
37
|
+
await (options.syncDirectory || syncParentDirectory)(directory);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
await handle?.close().catch(() => {});
|
|
40
|
+
await fs.unlink(temporaryPath).catch(() => {});
|
|
41
|
+
throw error;
|
|
42
|
+
} finally {
|
|
43
|
+
if (stagingDirectory) await fs.rm(stagingDirectory, { recursive: true, force: true }).catch(() => {});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function withPrivateFileLock(filePath, task, options = {}) {
|
|
48
|
+
const destination = path.resolve(filePath);
|
|
49
|
+
const directory = path.dirname(destination);
|
|
50
|
+
const lockPath = `${destination}.aipermission.lock`;
|
|
51
|
+
await ensurePrivateDirectory(directory, options);
|
|
52
|
+
await validatePrivateDestination(destination, options);
|
|
53
|
+
|
|
54
|
+
const ownerToken = randomBytes(16).toString("hex");
|
|
55
|
+
const ownerRecord = JSON.stringify({ pid: process.pid, token: ownerToken });
|
|
56
|
+
let handle;
|
|
57
|
+
for (let attempt = 0; attempt < (options.lockRetryLimit ?? lockRetryLimit); attempt += 1) {
|
|
58
|
+
let createdLock = false;
|
|
59
|
+
try {
|
|
60
|
+
handle = await fs.open(lockPath, "wx", 0o600);
|
|
61
|
+
createdLock = true;
|
|
62
|
+
await enforcePrivateFilePermissions(lockPath, options);
|
|
63
|
+
await handle.writeFile(`${ownerRecord}\n`, { encoding: "utf8" });
|
|
64
|
+
await handle.sync();
|
|
65
|
+
break;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await handle?.close().catch(() => {});
|
|
68
|
+
handle = undefined;
|
|
69
|
+
if (createdLock) await fs.unlink(lockPath).catch(() => {});
|
|
70
|
+
if (error.code !== "EEXIST") throw error;
|
|
71
|
+
await delay(options.lockRetryDelayMs ?? lockRetryDelayMs);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!handle) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Timed out waiting for private config lock: ${destination}. If no other MCP setup process is running, remove ${lockPath} and retry.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return await task();
|
|
82
|
+
} finally {
|
|
83
|
+
await handle.close().catch(() => {});
|
|
84
|
+
await releaseOwnedLock(lockPath, ownerToken);
|
|
85
|
+
await (options.syncDirectory || syncParentDirectory)(directory);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function privateTemporaryPath(filePath, suffix) {
|
|
90
|
+
return path.join(path.dirname(filePath), `.${path.basename(filePath)}.aipermission-${suffix}.tmp`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function privateTemporaryIgnorePath(filePath) {
|
|
94
|
+
return privateTemporaryPath(filePath, "*");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function privateStagingPath(filePath, suffix) {
|
|
98
|
+
return path.join(path.dirname(filePath), `.${path.basename(filePath)}.aipermission-stage-${suffix}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function privateStagingIgnorePath(filePath) {
|
|
102
|
+
return privateStagingPath(filePath, "*");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function privateLockPath(filePath) {
|
|
106
|
+
return `${filePath}.aipermission.lock`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function cleanupStalePrivateFiles(filePath, options = {}) {
|
|
110
|
+
const directory = path.dirname(filePath);
|
|
111
|
+
const prefix = `.${path.basename(filePath)}.aipermission-`;
|
|
112
|
+
const now = options.now ?? Date.now();
|
|
113
|
+
const maximumAge = options.staleAgeMs ?? staleTemporaryAgeMs;
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === "ENOENT") return;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const temporaryFile = entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith(".tmp");
|
|
123
|
+
const stagingDirectory = entry.isDirectory() && entry.name.startsWith(`${prefix}stage-`);
|
|
124
|
+
if (!temporaryFile && !stagingDirectory) continue;
|
|
125
|
+
const temporaryPath = path.join(directory, entry.name);
|
|
126
|
+
try {
|
|
127
|
+
const stat = await fs.lstat(temporaryPath);
|
|
128
|
+
if (now - stat.mtimeMs < maximumAge) continue;
|
|
129
|
+
if (stat.isFile()) await fs.unlink(temporaryPath);
|
|
130
|
+
if (stat.isDirectory()) await fs.rm(temporaryPath, { recursive: true, force: true });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error.code !== "ENOENT") throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function ensurePrivateDirectory(directory, options) {
|
|
138
|
+
const trustedRoot = path.resolve(options.trustedRoot || directory);
|
|
139
|
+
const resolvedDirectory = path.resolve(directory);
|
|
140
|
+
assertPathWithinRoot(trustedRoot, resolvedDirectory);
|
|
141
|
+
const missing = [];
|
|
142
|
+
const relativeParts = path.relative(trustedRoot, resolvedDirectory).split(path.sep).filter(Boolean);
|
|
143
|
+
let current = trustedRoot;
|
|
144
|
+
for (const part of ["", ...relativeParts]) {
|
|
145
|
+
if (part) current = path.join(current, part);
|
|
146
|
+
try {
|
|
147
|
+
const stat = await fs.lstat(current);
|
|
148
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing private config path through symbolic link or junction: ${current}`);
|
|
149
|
+
if (!stat.isDirectory()) throw new Error(`Private config parent is not a directory: ${current}`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error.code !== "ENOENT") throw error;
|
|
152
|
+
missing.push(current);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
await fs.mkdir(resolvedDirectory, { recursive: true, mode: 0o700 });
|
|
156
|
+
await rejectSymbolicPathComponents(trustedRoot, resolvedDirectory);
|
|
157
|
+
for (const created of missing) {
|
|
158
|
+
await fs.chmod(created, 0o700).catch((error) => {
|
|
159
|
+
if (process.platform !== "win32") throw error;
|
|
160
|
+
});
|
|
161
|
+
await (options.syncDirectory || syncParentDirectory)(path.dirname(created));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function createPrivateStagingDirectory(destination, options) {
|
|
166
|
+
const prefix = path.join(path.dirname(destination), `.${path.basename(destination)}.aipermission-stage-`);
|
|
167
|
+
const directory = await (options.makeTemporaryDirectory || fs.mkdtemp)(prefix);
|
|
168
|
+
try {
|
|
169
|
+
await enforcePrivateDirectoryPermissions(directory, options);
|
|
170
|
+
return directory;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function validatePrivateDestination(filePath, options) {
|
|
178
|
+
const trustedRoot = path.resolve(options.trustedRoot || path.dirname(filePath));
|
|
179
|
+
assertPathWithinRoot(trustedRoot, filePath);
|
|
180
|
+
await rejectSymbolicPathComponents(trustedRoot, path.dirname(filePath));
|
|
181
|
+
await rejectSymbolicLink(filePath);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function rejectSymbolicPathComponents(root, directory) {
|
|
185
|
+
const relativeParts = path.relative(root, directory).split(path.sep).filter(Boolean);
|
|
186
|
+
let current = root;
|
|
187
|
+
for (const part of ["", ...relativeParts]) {
|
|
188
|
+
if (part) current = path.join(current, part);
|
|
189
|
+
const stat = await fs.lstat(current);
|
|
190
|
+
if (stat.isSymbolicLink()) {
|
|
191
|
+
throw new Error(`Refusing private config path through symbolic link or junction: ${current}`);
|
|
192
|
+
}
|
|
193
|
+
if (!stat.isDirectory()) {
|
|
194
|
+
throw new Error(`Private config parent is not a directory: ${current}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function assertPathWithinRoot(root, destination) {
|
|
200
|
+
const relative = path.relative(path.resolve(root), path.resolve(destination));
|
|
201
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
|
|
202
|
+
throw new Error(`Refusing private config path outside trusted root: ${destination}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function rejectSymbolicLink(filePath) {
|
|
207
|
+
try {
|
|
208
|
+
const stat = await fs.lstat(filePath);
|
|
209
|
+
if (stat.isSymbolicLink()) {
|
|
210
|
+
throw new Error(`Refusing to replace symbolic-link config: ${filePath}`);
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error.code === "ENOENT") return;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function releaseOwnedLock(lockPath, ownerToken) {
|
|
219
|
+
try {
|
|
220
|
+
const owner = JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
221
|
+
if (owner?.token !== ownerToken) return;
|
|
222
|
+
await fs.unlink(lockPath);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError) return;
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function enforcePrivateFilePermissions(filePath, options) {
|
|
230
|
+
if (options.enforcePermissions) {
|
|
231
|
+
await options.enforcePermissions(filePath);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (operatingSystem(options) !== "win32") {
|
|
235
|
+
await fs.chmod(filePath, 0o600);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const sid = await currentWindowsSID(options);
|
|
239
|
+
await runWindowsSystemExecutable("icacls", [filePath, "/inheritance:r", "/grant:r", `*${sid}:(F)`], options);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function enforcePrivateDirectoryPermissions(directory, options) {
|
|
243
|
+
if (options.enforceDirectoryPermissions) {
|
|
244
|
+
await options.enforceDirectoryPermissions(directory);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (operatingSystem(options) !== "win32") {
|
|
248
|
+
await fs.chmod(directory, 0o700);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const sid = await currentWindowsSID(options);
|
|
252
|
+
await runWindowsSystemExecutable("icacls", [directory, "/inheritance:r", "/grant:r", `*${sid}:(OI)(CI)(F)`], options);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function currentWindowsSID(options) {
|
|
256
|
+
const { stdout } = await runWindowsSystemExecutable("whoami", ["/user", "/fo", "csv", "/nh"], options, {
|
|
257
|
+
encoding: "utf8",
|
|
258
|
+
});
|
|
259
|
+
const sid = stdout.match(/S-\d-(?:\d+-)+\d+/)?.[0];
|
|
260
|
+
if (!sid) throw new Error("Could not determine current Windows SID for private config ACL");
|
|
261
|
+
return sid;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function runWindowsSystemExecutable(name, args, options, executionOptions = {}) {
|
|
265
|
+
const execute = options.execFile || execFileAsync;
|
|
266
|
+
return execute(windowsSystemExecutable(name, options), args, {
|
|
267
|
+
windowsHide: true,
|
|
268
|
+
...executionOptions,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function windowsSystemExecutable(name, options) {
|
|
273
|
+
const systemRoot = options.windowsSystemRoot || process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
274
|
+
if (!path.win32.isAbsolute(systemRoot)) {
|
|
275
|
+
throw new Error("Windows SystemRoot must be an absolute path for private config ACL setup");
|
|
276
|
+
}
|
|
277
|
+
return path.win32.join(systemRoot, "System32", `${name}.exe`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function operatingSystem(options) {
|
|
281
|
+
return options.platform || process.platform;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function syncParentDirectory(directory) {
|
|
285
|
+
let handle;
|
|
286
|
+
try {
|
|
287
|
+
handle = await fs.open(directory, "r");
|
|
288
|
+
await handle.sync();
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (process.platform === "win32" && ["EACCES", "EINVAL", "ENOTSUP", "EPERM"].includes(error.code)) return;
|
|
291
|
+
throw error;
|
|
292
|
+
} finally {
|
|
293
|
+
await handle?.close().catch(() => {});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function delay(milliseconds) {
|
|
298
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
299
|
+
}
|
|
@@ -204,8 +204,11 @@ prefer this sequence:
|
|
|
204
204
|
type, headers, or existence.
|
|
205
205
|
5. Use `download_object` only for bounded object reads explicitly requested by
|
|
206
206
|
the operator.
|
|
207
|
-
6. Keep `overwrite=false` for `upload_object`
|
|
208
|
-
|
|
207
|
+
6. Keep `overwrite=false` for `upload_object` unless the operator explicitly
|
|
208
|
+
approved replacement. `rename_object` is intentionally unavailable because
|
|
209
|
+
S3-compatible APIs do not provide an atomic cross-key move. Keep the source
|
|
210
|
+
intact after creating a destination; deletion is a separate destructive
|
|
211
|
+
operator decision and must not be inferred from copy verification.
|
|
209
212
|
7. Treat `delete_object` as destructive and ask for explicit confirmation if
|
|
210
213
|
approval mode does not already provide it.
|
|
211
214
|
8. Use `presign_download` and `presign_upload` only for one exact object key
|
package/dist/server.js
CHANGED
|
@@ -30,7 +30,7 @@ server.tool(
|
|
|
30
30
|
{},
|
|
31
31
|
async () => {
|
|
32
32
|
return jsonToolResult(() => apiGet("/api/mcp/connector-targets"));
|
|
33
|
-
}
|
|
33
|
+
},
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
server.tool(
|
|
@@ -44,7 +44,7 @@ server.tool(
|
|
|
44
44
|
const params = new URLSearchParams({ target_ref });
|
|
45
45
|
return apiGet(`/api/mcp/connector-help?${params.toString()}`);
|
|
46
46
|
});
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
server.tool(
|
|
@@ -58,7 +58,7 @@ server.tool(
|
|
|
58
58
|
const params = new URLSearchParams({ target_ref });
|
|
59
59
|
return apiGet(`/api/mcp/connector-actions?${params.toString()}`);
|
|
60
60
|
});
|
|
61
|
-
}
|
|
61
|
+
},
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
server.tool(
|
|
@@ -69,17 +69,24 @@ server.tool(
|
|
|
69
69
|
action_name: z.string().min(1).describe("Action name from get_connector_actions."),
|
|
70
70
|
input: z.record(z.unknown()).optional().describe("Connector-specific action input."),
|
|
71
71
|
reason: z.string().optional().describe("Why this connector action is needed."),
|
|
72
|
-
idempotency_key: z
|
|
72
|
+
idempotency_key: z
|
|
73
|
+
.string()
|
|
74
|
+
.min(1)
|
|
75
|
+
.max(128)
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Caller-stable key that makes retries return the original request without running twice."),
|
|
73
78
|
},
|
|
74
79
|
async ({ target_ref, action_name, input, reason, idempotency_key }) => {
|
|
75
|
-
return jsonToolResult(() =>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
return jsonToolResult(() =>
|
|
81
|
+
apiPost("/api/mcp/connector-actions/call", {
|
|
82
|
+
target_ref,
|
|
83
|
+
action_name,
|
|
84
|
+
input: input || {},
|
|
85
|
+
reason: reason || "",
|
|
86
|
+
idempotency_key,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
},
|
|
83
90
|
);
|
|
84
91
|
|
|
85
92
|
server.tool(
|
|
@@ -90,7 +97,7 @@ server.tool(
|
|
|
90
97
|
},
|
|
91
98
|
async ({ request_id }) => {
|
|
92
99
|
return jsonToolResult(() => apiGet(`/api/mcp/connector-action-requests/${request_id}`));
|
|
93
|
-
}
|
|
100
|
+
},
|
|
94
101
|
);
|
|
95
102
|
|
|
96
103
|
server.tool(
|
|
@@ -104,7 +111,7 @@ server.tool(
|
|
|
104
111
|
const query = params.toString();
|
|
105
112
|
return apiGet(`/api/mcp/vault-items${query ? `?${query}` : ""}`);
|
|
106
113
|
});
|
|
107
|
-
}
|
|
114
|
+
},
|
|
108
115
|
);
|
|
109
116
|
|
|
110
117
|
server.tool(
|
|
@@ -112,14 +119,16 @@ server.tool(
|
|
|
112
119
|
"Run a Vault action under the configured project capability. Prompt waits for local approval; Always executes immediately through the same tracked request path. generate_item input accepts name, secret_type, generator_kind, provider, environment, description, expires_at, expiry_warning_days, tags (string array), usage_notes (array of {location, notes}), and shared_project_ids (integer array). restart_session_with_environment input requires target_ref and items with item_id, source_project_id, and optional replace_existing. Never include raw secret values.",
|
|
113
120
|
callVaultActionSchema,
|
|
114
121
|
async ({ project_ref, action_name, input, reason, idempotency_key }) => {
|
|
115
|
-
return jsonToolResult(() =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
return jsonToolResult(() =>
|
|
123
|
+
apiPost("/api/mcp/vault-actions/call", {
|
|
124
|
+
project_ref,
|
|
125
|
+
action_name,
|
|
126
|
+
input,
|
|
127
|
+
reason,
|
|
128
|
+
idempotency_key,
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
},
|
|
123
132
|
);
|
|
124
133
|
|
|
125
134
|
server.tool(
|
|
@@ -128,7 +137,7 @@ server.tool(
|
|
|
128
137
|
vaultActionRequestSchema,
|
|
129
138
|
async ({ request_id }) => {
|
|
130
139
|
return jsonToolResult(() => apiGet(`/api/mcp/vault-action-requests/${request_id}`));
|
|
131
|
-
}
|
|
140
|
+
},
|
|
132
141
|
);
|
|
133
142
|
|
|
134
143
|
server.tool(
|
|
@@ -137,7 +146,7 @@ server.tool(
|
|
|
137
146
|
vaultActionRequestSchema,
|
|
138
147
|
async ({ request_id }) => {
|
|
139
148
|
return jsonToolResult(() => apiPost(`/api/mcp/vault-action-requests/${request_id}/cancel`, {}));
|
|
140
|
-
}
|
|
149
|
+
},
|
|
141
150
|
);
|
|
142
151
|
|
|
143
152
|
const transport = new StdioServerTransport();
|
|
@@ -190,7 +199,7 @@ async function apiFetch(path, options) {
|
|
|
190
199
|
});
|
|
191
200
|
} catch (error) {
|
|
192
201
|
if (error?.name === "AbortError") {
|
|
193
|
-
throw new Error(`AIPermission API request timed out after ${timeout}ms
|
|
202
|
+
throw new Error(`AIPermission API request timed out after ${timeout}ms`, { cause: error });
|
|
194
203
|
}
|
|
195
204
|
throw error;
|
|
196
205
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipermission/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
4
4
|
"mcpName": "io.github.aipermission/aipermission-mcp",
|
|
5
5
|
"description": "Local-first MCP bridge for the aipermission gateway.",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node scripts/build.js",
|
|
31
|
+
"format": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"scripts/**/*.js\"",
|
|
32
|
+
"format:check": "prettier --check \"src/**/*.js\" \"test/**/*.js\" \"scripts/**/*.js\"",
|
|
33
|
+
"lint": "eslint src test scripts",
|
|
31
34
|
"prepack": "npm run build",
|
|
32
35
|
"test": "node --test test/*.test.js",
|
|
33
36
|
"start": "node dist/cli.js",
|
|
@@ -36,8 +39,21 @@
|
|
|
36
39
|
"engines": {
|
|
37
40
|
"node": ">=20"
|
|
38
41
|
},
|
|
42
|
+
"devEngines": {
|
|
43
|
+
"runtime": {
|
|
44
|
+
"name": "node",
|
|
45
|
+
"version": ">=20.19.0",
|
|
46
|
+
"onFail": "error"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
39
49
|
"dependencies": {
|
|
40
50
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
41
51
|
"zod": "3.25.76"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
55
|
+
"eslint": "^10.9.1",
|
|
56
|
+
"globals": "^17.11.0",
|
|
57
|
+
"prettier": "^3.9.6"
|
|
42
58
|
}
|
|
43
59
|
}
|
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.aipermission/aipermission-mcp",
|
|
4
4
|
"title": "AIPermission",
|
|
5
5
|
"description": "Local-first MCP bridge for the AIPermission gateway.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.38",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@aipermission/mcp",
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.38",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
}
|