@aipermission/mcp 0.2.57 → 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 +40 -14
- 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,
|
|
@@ -86,13 +87,25 @@ async function runConfiguration(command, argv) {
|
|
|
86
87
|
throw new Error("API token is required.");
|
|
87
88
|
}
|
|
88
89
|
const config = adaptMCPServerConfig(provider.id, buildMCPServerConfig({ apiUrl, token }));
|
|
89
|
-
const skillResult = preparedSkill ? await reportInstalledSkill(preparedSkill) : undefined;
|
|
90
90
|
const result = await writeProviderConfig(provider.id, name, config, {
|
|
91
91
|
force: Boolean(flags.force),
|
|
92
92
|
scope: flags.scope,
|
|
93
93
|
homeDir: flags.home,
|
|
94
94
|
projectDir: flags.projectDir,
|
|
95
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
|
+
}
|
|
96
109
|
console.log("");
|
|
97
110
|
console.log(`${color.green}Configured ${provider.label}${color.reset}`);
|
|
98
111
|
console.log(`${color.dim}Name:${color.reset} ${name}`);
|
|
@@ -316,6 +329,7 @@ export async function writeProviderConfig(providerID, name, config, options = {}
|
|
|
316
329
|
const trustedRoot = target.trustedRoot;
|
|
317
330
|
const writeOptions = {
|
|
318
331
|
trustedRoot,
|
|
332
|
+
jsonc: providerID === "vscode",
|
|
319
333
|
beforeWrite: target.projectConfig
|
|
320
334
|
? async () => {
|
|
321
335
|
await assertProjectConfigWritable(target.path, options);
|
|
@@ -337,27 +351,39 @@ export async function writeJSONMCPConfig(filePath, name, config, rootKey, option
|
|
|
337
351
|
filePath,
|
|
338
352
|
async () => {
|
|
339
353
|
await options.beforeWrite?.();
|
|
340
|
-
let
|
|
354
|
+
let content = "";
|
|
341
355
|
try {
|
|
342
|
-
|
|
356
|
+
content = await fs.readFile(filePath, "utf8");
|
|
343
357
|
} catch (error) {
|
|
344
358
|
if (error.code !== "ENOENT") {
|
|
345
|
-
|
|
346
|
-
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`, {
|
|
347
360
|
cause: error,
|
|
348
361
|
});
|
|
349
362
|
}
|
|
350
363
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
385
|
await options.beforeWrite?.();
|
|
360
|
-
await writePrivateFile(filePath,
|
|
386
|
+
await writePrivateFile(filePath, outputContent, options);
|
|
361
387
|
},
|
|
362
388
|
options,
|
|
363
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
|
}
|