@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 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 root = {};
354
+ let content = "";
341
355
  try {
342
- root = JSON.parse(await fs.readFile(filePath, "utf8"));
356
+ content = await fs.readFile(filePath, "utf8");
343
357
  } catch (error) {
344
358
  if (error.code !== "ENOENT") {
345
- redactParseError(error);
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
- if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
352
- const currentServers = root[rootKey];
353
- const servers =
354
- currentServers && typeof currentServers === "object" && !Array.isArray(currentServers)
355
- ? { ...currentServers }
356
- : Object.create(null);
357
- Object.defineProperty(servers, name, { value: config, enumerable: true, configurable: true, writable: true });
358
- root[rootKey] = servers;
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, `${JSON.stringify(root, null, 2)}\n`, options);
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.text();
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.57",
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.57",
6
+ "version": "0.2.58",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.57",
11
+ "version": "0.2.58",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }