@aipermission/mcp 0.2.56 → 0.2.58
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 +7 -0
- package/dist/init.js +50 -19
- package/dist/jsonc-config.js +33 -0
- package/dist/response-body.js +23 -0
- package/dist/server.js +2 -1
- package/package.json +2 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,11 @@ configuration, and installs the native operator skill for the selected client.
|
|
|
41
41
|
Generated runtime configs pin the exact package version that wrote them; re-run
|
|
42
42
|
setup when you intentionally upgrade a client. Use `init` when you only want the
|
|
43
43
|
MCP config, or `install-skill` when you only want the skill.
|
|
44
|
+
VS Code `.vscode/mcp.json` may contain JSONC comments and trailing commas;
|
|
45
|
+
setup updates only the selected server entry while keeping unrelated entries
|
|
46
|
+
and comments. Setup validates the skill before writing the config, then installs
|
|
47
|
+
it. If skill installation fails after the config is written, the error names
|
|
48
|
+
that partial state so you can run `install-skill` separately.
|
|
44
49
|
|
|
45
50
|
Check both paths without printing the bearer token:
|
|
46
51
|
|
|
@@ -91,6 +96,8 @@ integer; invalid configuration stops the bridge instead of silently changing
|
|
|
91
96
|
the deadline. It covers both response headers and the complete response body,
|
|
92
97
|
including streamed bodies. A timeout does not prove that a submitted
|
|
93
98
|
operation failed; do not retry mutations with a new idempotency key blindly.
|
|
99
|
+
Gateway response bodies are capped at 8 MiB. An oversized POST response is
|
|
100
|
+
also treated as an unknown outcome, not proof that the operation failed.
|
|
94
101
|
If a POST response is lost or incomplete, the bridge returns
|
|
95
102
|
`status: outcome_unknown` and `code: gateway_transport_outcome_unknown`.
|
|
96
103
|
Action calls also return the original `idempotency_key`; no request ID is
|
package/dist/init.js
CHANGED
|
@@ -11,6 +11,7 @@ import { parseCommandFlags } from "./cli-flags.js";
|
|
|
11
11
|
import { DEFAULT_API_URL, normalizeLocalAPIURL } from "./local-url.js";
|
|
12
12
|
import { adaptMCPServerConfig, getClient, MCP_PROVIDERS, resolveMCPConfigTarget, resolveMCPPrintTarget } from "./client-registry.js";
|
|
13
13
|
import { commitSkillInstallation, prepareSkillInstallation } from "./install-skill.js";
|
|
14
|
+
import { updateJSONCServer } from "./jsonc-config.js";
|
|
14
15
|
import {
|
|
15
16
|
atomicWritePrivateFile,
|
|
16
17
|
privateLockPath,
|
|
@@ -50,12 +51,18 @@ async function runConfiguration(command, argv) {
|
|
|
50
51
|
const flags = parseCommandFlags(command, argv);
|
|
51
52
|
const interactive = Boolean(input.isTTY && output.isTTY);
|
|
52
53
|
assertProviderSelectionAvailable(flags.provider, interactive);
|
|
53
|
-
|
|
54
|
+
let rl;
|
|
55
|
+
const getReadline = () => {
|
|
56
|
+
rl ||= readline.createInterface({ input, output });
|
|
57
|
+
return rl;
|
|
58
|
+
};
|
|
54
59
|
try {
|
|
55
60
|
const provider = flags.provider
|
|
56
61
|
? findProvider(flags.provider)
|
|
57
62
|
: await selectProvider("Which AI client should use this token?", MCP_PROVIDERS);
|
|
58
|
-
const
|
|
63
|
+
const needsToken = provider.id !== "custom" && !flags.print;
|
|
64
|
+
const stdinToken = needsToken && flags.tokenStdin ? (await readStdin()).trim() : "";
|
|
65
|
+
const name = sanitizeName(flags.name || (interactive ? await ask(getReadline(), "MCP server name", "aipermission") : "aipermission"));
|
|
59
66
|
const apiUrl = normalizeURL(flags.apiUrl || DEFAULT_API_URL);
|
|
60
67
|
const outputTarget =
|
|
61
68
|
provider.id === "custom"
|
|
@@ -75,19 +82,30 @@ async function runConfiguration(command, argv) {
|
|
|
75
82
|
return { provider: provider.id, name, printed: true, skill: skillResult };
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
const
|
|
79
|
-
const token = await resolveToken({ ...flags, stdinToken }, rl);
|
|
85
|
+
const token = flags.tokenStdin ? stdinToken : await resolveToken(flags, getReadline());
|
|
80
86
|
if (!token) {
|
|
81
87
|
throw new Error("API token is required.");
|
|
82
88
|
}
|
|
83
89
|
const config = adaptMCPServerConfig(provider.id, buildMCPServerConfig({ apiUrl, token }));
|
|
84
|
-
const skillResult = preparedSkill ? await reportInstalledSkill(preparedSkill) : undefined;
|
|
85
90
|
const result = await writeProviderConfig(provider.id, name, config, {
|
|
86
91
|
force: Boolean(flags.force),
|
|
87
92
|
scope: flags.scope,
|
|
88
93
|
homeDir: flags.home,
|
|
89
94
|
projectDir: flags.projectDir,
|
|
90
95
|
});
|
|
96
|
+
let skillResult;
|
|
97
|
+
if (preparedSkill) {
|
|
98
|
+
try {
|
|
99
|
+
skillResult = await reportInstalledSkill(preparedSkill);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`MCP config was written at ${result.path}, but operator skill installation failed. Complete the skill installation separately.`,
|
|
103
|
+
{
|
|
104
|
+
cause: error,
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
91
109
|
console.log("");
|
|
92
110
|
console.log(`${color.green}Configured ${provider.label}${color.reset}`);
|
|
93
111
|
console.log(`${color.dim}Name:${color.reset} ${name}`);
|
|
@@ -103,7 +121,7 @@ async function runConfiguration(command, argv) {
|
|
|
103
121
|
console.log(`${color.yellow}Restart the AI client so it reloads MCP servers.${color.reset}`);
|
|
104
122
|
return { provider: provider.id, name, config: result, skill: skillResult };
|
|
105
123
|
} finally {
|
|
106
|
-
rl
|
|
124
|
+
rl?.close();
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -311,6 +329,7 @@ export async function writeProviderConfig(providerID, name, config, options = {}
|
|
|
311
329
|
const trustedRoot = target.trustedRoot;
|
|
312
330
|
const writeOptions = {
|
|
313
331
|
trustedRoot,
|
|
332
|
+
jsonc: providerID === "vscode",
|
|
314
333
|
beforeWrite: target.projectConfig
|
|
315
334
|
? async () => {
|
|
316
335
|
await assertProjectConfigWritable(target.path, options);
|
|
@@ -332,27 +351,39 @@ export async function writeJSONMCPConfig(filePath, name, config, rootKey, option
|
|
|
332
351
|
filePath,
|
|
333
352
|
async () => {
|
|
334
353
|
await options.beforeWrite?.();
|
|
335
|
-
let
|
|
354
|
+
let content = "";
|
|
336
355
|
try {
|
|
337
|
-
|
|
356
|
+
content = await fs.readFile(filePath, "utf8");
|
|
338
357
|
} catch (error) {
|
|
339
358
|
if (error.code !== "ENOENT") {
|
|
340
|
-
|
|
341
|
-
throw new Error(`Could not parse JSON config at ${filePath}; the existing file was left unchanged`, {
|
|
359
|
+
throw new Error(`Could not read JSON config at ${filePath}; the existing file was left unchanged`, {
|
|
342
360
|
cause: error,
|
|
343
361
|
});
|
|
344
362
|
}
|
|
345
363
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
let outputContent;
|
|
365
|
+
if (options.jsonc) {
|
|
366
|
+
outputContent = updateJSONCServer(content, rootKey, name, config);
|
|
367
|
+
} else {
|
|
368
|
+
let root;
|
|
369
|
+
try {
|
|
370
|
+
root = content ? JSON.parse(content) : {};
|
|
371
|
+
} catch (error) {
|
|
372
|
+
redactParseError(error);
|
|
373
|
+
throw new Error(`Could not parse JSON config at ${filePath}; the existing file was left unchanged`, { cause: error });
|
|
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
|
+
outputContent = `${JSON.stringify(root, null, 2)}\n`;
|
|
384
|
+
}
|
|
354
385
|
await options.beforeWrite?.();
|
|
355
|
-
await writePrivateFile(filePath,
|
|
386
|
+
await writePrivateFile(filePath, outputContent, options);
|
|
356
387
|
},
|
|
357
388
|
options,
|
|
358
389
|
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
2
|
+
|
|
3
|
+
export function updateJSONCServer(content, rootKey, name, config) {
|
|
4
|
+
const source = content.trim() ? content : "{}\n";
|
|
5
|
+
const errors = [];
|
|
6
|
+
const root = parse(source, errors, { allowTrailingComma: true });
|
|
7
|
+
if (errors.length || !root || typeof root !== "object" || Array.isArray(root)) {
|
|
8
|
+
throw new Error("Could not parse VS Code JSONC config; the existing file was left unchanged.");
|
|
9
|
+
}
|
|
10
|
+
const servers = root[rootKey];
|
|
11
|
+
if (servers !== undefined && (!servers || typeof servers !== "object" || Array.isArray(servers))) {
|
|
12
|
+
throw new Error("VS Code MCP servers must be an object; the existing file was left unchanged.");
|
|
13
|
+
}
|
|
14
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
15
|
+
let next;
|
|
16
|
+
try {
|
|
17
|
+
next = applyEdits(
|
|
18
|
+
source,
|
|
19
|
+
modify(source, [rootKey, name], config, {
|
|
20
|
+
getInsertionIndex: () => 0,
|
|
21
|
+
formattingOptions: { insertSpaces: true, tabSize: 2, eol },
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("Could not safely update VS Code JSONC config; the existing file was left unchanged.");
|
|
26
|
+
}
|
|
27
|
+
const nextErrors = [];
|
|
28
|
+
const updated = parse(next, nextErrors, { allowTrailingComma: true });
|
|
29
|
+
if (nextErrors.length || JSON.stringify(updated?.[rootKey]?.[name]) !== JSON.stringify(config)) {
|
|
30
|
+
throw new Error("Could not safely update VS Code JSONC config; the existing file was left unchanged.");
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const MAX_GATEWAY_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
export async function readGatewayResponseText(response) {
|
|
4
|
+
if (!response.body) return "";
|
|
5
|
+
const reader = response.body.getReader();
|
|
6
|
+
const chunks = [];
|
|
7
|
+
let received = 0;
|
|
8
|
+
try {
|
|
9
|
+
for (;;) {
|
|
10
|
+
const { done, value } = await reader.read();
|
|
11
|
+
if (done) break;
|
|
12
|
+
received += value.byteLength;
|
|
13
|
+
if (received > MAX_GATEWAY_RESPONSE_BYTES) {
|
|
14
|
+
await reader.cancel();
|
|
15
|
+
throw new Error(`Gateway response exceeds ${MAX_GATEWAY_RESPONSE_BYTES} bytes.`);
|
|
16
|
+
}
|
|
17
|
+
chunks.push(value);
|
|
18
|
+
}
|
|
19
|
+
} finally {
|
|
20
|
+
reader.releaseLock();
|
|
21
|
+
}
|
|
22
|
+
return new TextDecoder().decode(Buffer.concat(chunks, received));
|
|
23
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { idempotencyKeySchema } from "./idempotency-key.js";
|
|
|
18
18
|
import { normalizeLocalAPIURL } from "./local-url.js";
|
|
19
19
|
import { projectGatewaySuccess, responseContracts } from "./response-contracts.js";
|
|
20
20
|
import { jsonActionToolResult, jsonToolResult } from "./results.js";
|
|
21
|
+
import { readGatewayResponseText } from "./response-body.js";
|
|
21
22
|
import { externalActionAnnotations, localMutationAnnotations, localReadAnnotations } from "./tool-annotations.js";
|
|
22
23
|
|
|
23
24
|
const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -266,7 +267,7 @@ async function apiRequest(path, options, idempotencyKey, context = {}) {
|
|
|
266
267
|
}
|
|
267
268
|
dispatchStarted = true;
|
|
268
269
|
const response = await fetch(request, { signal: controller.signal });
|
|
269
|
-
const text = await response
|
|
270
|
+
const text = await readGatewayResponseText(response);
|
|
270
271
|
const data = response.status === 204 ? null : parseResponseBody(text);
|
|
271
272
|
bodyReceived = true;
|
|
272
273
|
if (!response.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipermission/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.58",
|
|
4
4
|
"mcpName": "io.github.aipermission/aipermission-mcp",
|
|
5
5
|
"description": "Local-only MCP bridge for the aipermission gateway.",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
53
|
+
"jsonc-parser": "3.3.1",
|
|
53
54
|
"smol-toml": "1.8.0",
|
|
54
55
|
"yaml": "2.9.1",
|
|
55
56
|
"zod": "3.25.76"
|
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.aipermission/aipermission-mcp",
|
|
4
4
|
"title": "AIPermission",
|
|
5
5
|
"description": "Local-only MCP bridge for the AIPermission gateway.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.58",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@aipermission/mcp",
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.58",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
}
|