@aipermission/mcp 0.1.13 → 0.2.0

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/dist/server.js CHANGED
@@ -6,368 +6,87 @@ if (process.argv[2] === "init") {
6
6
  process.exit(0);
7
7
  }
8
8
 
9
- import fs from "node:fs";
10
- import fsp from "node:fs/promises";
11
- import os from "node:os";
12
- import path from "node:path";
13
- import { Readable } from "node:stream";
14
- import { pipeline } from "node:stream/promises";
9
+ import { readFileSync } from "node:fs";
15
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
12
  import { z } from "zod";
18
13
  import { normalizeLocalAPIURL } from "./local-url.js";
19
14
  import { jsonToolResult } from "./results.js";
20
15
 
16
+ const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
21
17
  const apiUrl = normalizeLocalAPIURL(process.env.AIPERMISSION_API_URL);
22
18
  const apiToken = process.env.AIPERMISSION_API_TOKEN || "";
23
19
  const apiTimeoutMs = Number.parseInt(process.env.AIPERMISSION_HTTP_TIMEOUT_MS || "60000", 10);
24
- const apiTransferTimeoutMs = Number.parseInt(process.env.AIPERMISSION_TRANSFER_TIMEOUT_MS || "7200000", 10);
25
20
 
26
21
  const server = new McpServer({
27
22
  name: "aipermission",
28
- version: "0.1.13",
23
+ version: packageMetadata.version,
29
24
  });
30
25
 
31
26
  server.tool(
32
- "list_servers",
33
- "List servers this aipermission token can access. Credentials are never returned.",
27
+ "list_connector_targets",
28
+ "List connector targets this AIPermission token can access. Credentials and secrets are never returned.",
34
29
  {},
35
30
  async () => {
36
- return jsonToolResult(() => apiGet("/api/mcp/servers"));
31
+ return jsonToolResult(() => apiGet("/api/mcp/connector-targets"));
37
32
  }
38
33
  );
39
34
 
40
35
  server.tool(
41
- "exec",
42
- "Execute a shell command on one allowed server, or the same shell command across multiple allowed servers, through the local aipermission gateway. If status is approval_pending, follow assistant_hint and poll get_request. Long always_run commands return running; use read_console to continue watching.",
36
+ "get_connector_help",
37
+ "Read AI-facing help for one connector target/profile. Use this before calling connector actions for the first time.",
43
38
  {
44
- server_id: z.number().int().positive().optional().describe("Single server id from list_servers. Use either server_id or server_ids, not both."),
45
- server_ids: z.array(z.number().int().positive()).min(1).max(25).optional().describe("Multiple server ids from list_servers for bulk execution. Up to 25 targets. Use either server_id or server_ids, not both."),
46
- command: z.string().min(1).describe("Shell command to execute."),
47
- reason: z.string().optional().describe("Why this command is needed. Required when using server_ids."),
39
+ target_ref: z.string().min(1).describe("Target ref from list_connector_targets in connector:target_id:profile_id format."),
48
40
  },
49
- async ({ server_id, server_ids, command, reason }) => {
41
+ async ({ target_ref }) => {
50
42
  return jsonToolResult(() => {
51
- if (server_id && server_ids?.length) {
52
- throw new Error("Use either server_id or server_ids, not both.");
53
- }
54
- if (!server_id && !server_ids?.length) {
55
- throw new Error("server_id or server_ids is required.");
56
- }
57
- if (server_ids?.length) {
58
- if (!String(reason || "").trim()) {
59
- throw new Error("reason is required when using server_ids.");
60
- }
61
- return apiPost("/api/mcp/bulk-exec", {
62
- server_ids,
63
- command,
64
- reason,
65
- });
66
- }
67
- return apiPost("/api/mcp/exec", {
68
- server_id,
69
- command,
70
- reason: reason || "",
71
- });
43
+ const params = new URLSearchParams({ target_ref });
44
+ return apiGet(`/api/mcp/connector-help?${params.toString()}`);
72
45
  });
73
46
  }
74
47
  );
75
48
 
76
49
  server.tool(
77
- "read_console",
78
- "Read the latest persistent console transcript for one allowed server, or for multiple allowed servers after a multi-server exec. Use this after a long-running exec returns running.",
50
+ "get_connector_actions",
51
+ "List actions exposed by one connector target/profile. Action execution is still checked against token permissions.",
79
52
  {
80
- server_id: z.number().int().positive().optional().describe("Single server id from list_servers. Use either server_id or server_ids, not both."),
81
- server_ids: z.array(z.number().int().positive()).min(1).max(25).optional().describe("Multiple server ids from list_servers. Up to 25 targets. Use either server_id or server_ids, not both."),
82
- tail: z.number().int().positive().max(100000).optional().describe("Maximum transcript characters to return."),
53
+ target_ref: z.string().min(1).describe("Target ref from list_connector_targets in connector:target_id:profile_id format."),
83
54
  },
84
- async ({ server_id, server_ids, tail }) => {
85
- return jsonToolResult(async () => {
86
- if (server_id && server_ids?.length) {
87
- throw new Error("Use either server_id or server_ids, not both.");
88
- }
89
- if (!server_id && !server_ids?.length) {
90
- throw new Error("server_id or server_ids is required.");
91
- }
92
- const readOne = (id) => {
93
- const params = new URLSearchParams({ server_id: String(id) });
94
- if (tail) {
95
- params.set("tail", String(tail));
96
- }
97
- return apiGet(`/api/mcp/console?${params.toString()}`);
98
- };
99
- if (server_ids?.length) {
100
- const unique = new Set(server_ids);
101
- if (unique.size !== server_ids.length) {
102
- throw new Error("server_ids must not contain duplicates.");
103
- }
104
- const items = await Promise.all(server_ids.map(async (id) => {
105
- try {
106
- return await readOne(id);
107
- } catch (error) {
108
- return {
109
- status: "error",
110
- server_id: id,
111
- error: error?.message || "failed to read console",
112
- };
113
- }
114
- }));
115
- return {
116
- status: "ok",
117
- items,
118
- assistant_hint: "Inspect each item independently. A listed server may have no active console, blocked read permission, or an SSH/session error.",
119
- };
120
- }
121
- return readOne(server_id);
122
- });
123
- }
124
- );
125
-
126
- server.tool(
127
- "restart_console_session",
128
- "Restart the persistent console session for a server when it appears stuck. This closes the current gateway-owned console session and the next exec will open a fresh SSH session.",
129
- {
130
- server_id: z.number().int().positive().describe("Server id from list_servers."),
131
- },
132
- async ({ server_id }) => {
133
- return jsonToolResult(() => apiPost("/api/mcp/console/restart", {
134
- server_id,
135
- }));
136
- }
137
- );
138
-
139
- server.tool(
140
- "get_request",
141
- "Read an aipermission command request by id. Use this after exec returns approval_pending or running.",
142
- {
143
- request_id: z.number().int().positive().describe("Request id returned by exec."),
144
- },
145
- async ({ request_id }) => {
146
- return jsonToolResult(() => apiGet(`/api/mcp/requests/${request_id}`));
147
- }
148
- );
149
-
150
- server.tool(
151
- "list_requests",
152
- "List command requests for this token. Optionally filter by status such as pending_approval, running, completed, failed, declined, or error.",
153
- {
154
- status: z.string().optional().describe("Optional request status filter."),
155
- },
156
- async ({ status }) => {
55
+ async ({ target_ref }) => {
157
56
  return jsonToolResult(() => {
158
- const params = new URLSearchParams();
159
- if (status) {
160
- params.set("status", status);
161
- }
162
- const suffix = params.toString() ? `?${params.toString()}` : "";
163
- return apiGet(`/api/mcp/requests${suffix}`);
57
+ const params = new URLSearchParams({ target_ref });
58
+ return apiGet(`/api/mcp/connector-actions?${params.toString()}`);
164
59
  });
165
60
  }
166
61
  );
167
62
 
168
63
  server.tool(
169
- "send_message",
170
- "Send a short note to the aipermission Console messages panel for the human operator.",
64
+ "call_connector_action",
65
+ "Call one connector action through AIPermission. If status is approval_pending or running, follow assistant_hint and poll get_connector_action_request.",
171
66
  {
172
- message: z.string().min(1).describe("Message to show in the Console messages panel."),
173
- server_id: z.number().int().positive().optional().describe("Optional server id this message is about."),
174
- session_id: z.number().int().positive().optional().describe("Optional console session id this message is about."),
67
+ target_ref: z.string().min(1).describe("Target ref from list_connector_targets."),
68
+ action_name: z.string().min(1).describe("Action name from get_connector_actions."),
69
+ input: z.record(z.unknown()).optional().describe("Connector-specific action input."),
70
+ reason: z.string().optional().describe("Why this connector action is needed."),
175
71
  },
176
- async ({ message, server_id, session_id }) => {
177
- return jsonToolResult(() => apiPost("/api/mcp/messages", {
178
- message,
179
- server_id: server_id || null,
180
- session_id: session_id || null,
72
+ async ({ target_ref, action_name, input, reason }) => {
73
+ return jsonToolResult(() => apiPost("/api/mcp/connector-actions/call", {
74
+ target_ref,
75
+ action_name,
76
+ input: input || {},
77
+ reason: reason || "",
181
78
  }));
182
79
  }
183
80
  );
184
81
 
185
82
  server.tool(
186
- "list_file_transfers",
187
- "List file transfer records visible to this token. Results never include local temp paths, archive paths, local upload contents, or file contents.",
188
- {
189
- server_id: z.number().int().positive().optional().describe("Optional server id from list_servers."),
190
- direction: z.enum(["upload", "download"]).optional().describe("Optional transfer direction."),
191
- status: z.enum(["pending_approval", "pending", "running", "paused", "completed", "failed", "canceled"]).optional().describe("Optional transfer status."),
192
- limit: z.number().int().positive().max(100).optional().describe("Maximum records to return."),
193
- offset: z.number().int().min(0).optional().describe("Pagination offset."),
194
- },
195
- async ({ server_id, direction, status, limit, offset }) => {
196
- return jsonToolResult(() => {
197
- const params = new URLSearchParams();
198
- if (server_id) params.set("server_id", String(server_id));
199
- if (direction) params.set("direction", direction);
200
- if (status) params.set("status", status);
201
- if (limit) params.set("limit", String(limit));
202
- if (offset) params.set("offset", String(offset));
203
- const suffix = params.toString() ? `?${params.toString()}` : "";
204
- return apiGet(`/api/mcp/file-transfers${suffix}`);
205
- });
206
- }
207
- );
208
-
209
- server.tool(
210
- "get_file_transfer",
211
- "Read one file transfer record visible to this token. Local temp paths, archive paths, local upload contents, and file contents are never returned.",
212
- {
213
- transfer_id: z.number().int().positive().describe("Transfer id from list_file_transfers or get_file_transfer_batch."),
214
- },
215
- async ({ transfer_id }) => {
216
- return jsonToolResult(() => apiGet(`/api/mcp/file-transfers/${transfer_id}`));
217
- }
218
- );
219
-
220
- server.tool(
221
- "list_file_transfer_batches",
222
- "List file transfer queues visible to this token. Use get_file_transfer_batch for per-file progress.",
223
- {
224
- server_id: z.number().int().positive().optional().describe("Optional server id from list_servers."),
225
- direction: z.enum(["upload", "download"]).optional().describe("Optional transfer direction."),
226
- status: z.enum(["pending_approval", "pending", "running", "paused", "completed", "failed", "canceled"]).optional().describe("Optional transfer status."),
227
- limit: z.number().int().positive().max(100).optional().describe("Maximum records to return."),
228
- offset: z.number().int().min(0).optional().describe("Pagination offset."),
229
- },
230
- async ({ server_id, direction, status, limit, offset }) => {
231
- return jsonToolResult(() => {
232
- const params = new URLSearchParams();
233
- if (server_id) params.set("server_id", String(server_id));
234
- if (direction) params.set("direction", direction);
235
- if (status) params.set("status", status);
236
- if (limit) params.set("limit", String(limit));
237
- if (offset) params.set("offset", String(offset));
238
- const suffix = params.toString() ? `?${params.toString()}` : "";
239
- return apiGet(`/api/mcp/file-transfer-batches${suffix}`);
240
- });
241
- }
242
- );
243
-
244
- server.tool(
245
- "get_file_transfer_batch",
246
- "Read one file transfer queue with per-file progress. Use save_file_download to write a completed MCP-started download to an explicit local path.",
247
- {
248
- batch_id: z.number().int().positive().describe("Batch id from list_file_transfer_batches or start_file_download."),
249
- },
250
- async ({ batch_id }) => {
251
- return jsonToolResult(() => apiGet(`/api/mcp/file-transfer-batches/${batch_id}`));
252
- }
253
- );
254
-
255
- server.tool(
256
- "browse_remote_files",
257
- "Browse a remote server directory through AIPermission. Requires always_run permission. This lists remote metadata only and does not read local files.",
258
- {
259
- server_id: z.number().int().positive().describe("Server id from list_servers."),
260
- path: z.string().optional().describe("Absolute remote directory path. Defaults to /."),
261
- },
262
- async ({ server_id, path }) => {
263
- return jsonToolResult(() => apiPost("/api/mcp/file-transfers/browse", {
264
- server_id,
265
- path: path || "/",
266
- }));
267
- }
268
- );
269
-
270
- server.tool(
271
- "start_file_download",
272
- "Start a remote file download queue through AIPermission. always_run starts immediately; approval_required creates a local approval queue. Use get_file_transfer_batch for progress, then save_file_download to write a completed download to the local machine.",
273
- {
274
- server_id: z.number().int().positive().describe("Server id from list_servers."),
275
- remote_paths: z.array(z.string().min(1)).min(1).max(100).describe("Absolute remote file paths to download sequentially."),
276
- archive_name: z.string().optional().describe("Optional archive filename for multi-file downloads."),
277
- },
278
- async ({ server_id, remote_paths, archive_name }) => {
279
- return jsonToolResult(() => apiPost("/api/mcp/file-transfers/download-batch", {
280
- server_id,
281
- remote_paths,
282
- archive_name: archive_name || "",
283
- }));
284
- }
285
- );
286
-
287
- server.tool(
288
- "save_file_download",
289
- "Save a completed MCP-started download batch to the local filesystem. File contents are written by the local MCP process and are not returned to the AI response.",
290
- {
291
- batch_id: z.number().int().positive().describe("Completed download batch id from start_file_download or list_file_transfer_batches."),
292
- local_path: z.string().min(1).describe("Local file path or existing directory where the completed download should be saved."),
293
- overwrite: z.boolean().optional().describe("Whether to overwrite an existing local file. Defaults to false."),
294
- },
295
- async ({ batch_id, local_path, overwrite }) => {
296
- return jsonToolResult(async () => {
297
- const batch = await apiGet(`/api/mcp/file-transfer-batches/${batch_id}`);
298
- if (batch?.direction !== "download") {
299
- throw new Error("batch is not a download");
300
- }
301
- if (batch?.status !== "completed") {
302
- throw new Error(`download batch is not completed; current status is ${batch?.status || "unknown"}`);
303
- }
304
- const filename = suggestedDownloadFilename(batch);
305
- const destination = await resolveLocalDestination(local_path, filename, Boolean(overwrite));
306
- const saved = await apiDownloadToFile(`/api/mcp/file-transfer-batches/${batch_id}/download`, destination, Boolean(overwrite));
307
- return {
308
- status: "saved",
309
- batch_id,
310
- local_path: saved.path,
311
- file_name: path.basename(saved.path),
312
- bytes_written: saved.bytes,
313
- assistant_hint: "The file was saved by the local MCP process. Do not print file contents unless the user explicitly asks you to inspect the saved file.",
314
- };
315
- });
316
- }
317
- );
318
-
319
- server.tool(
320
- "upload_files",
321
- "Upload local files to a remote server through AIPermission. always_run starts immediately; approval_required stages the files locally and waits for local approval before writing to the remote server. File contents are read by the local MCP process and are not returned to the AI response.",
322
- {
323
- server_id: z.number().int().positive().describe("Server id from list_servers."),
324
- local_paths: z.array(z.string().min(1)).min(1).max(100).describe("Local file paths to upload sequentially."),
325
- remote_dir: z.string().min(1).describe("Absolute remote directory where files should be uploaded."),
326
- overwrite: z.boolean().optional().describe("Whether to overwrite existing remote files. Defaults to false."),
327
- },
328
- async ({ server_id, local_paths, remote_dir, overwrite }) => {
329
- return jsonToolResult(async () => {
330
- const files = await resolveUploadFiles(local_paths);
331
- const batch = await apiPostMultipart("/api/mcp/file-transfers/upload-batch", {
332
- server_id: String(server_id),
333
- remote_dir,
334
- overwrite: overwrite ? "true" : "false",
335
- }, files);
336
- return batch;
337
- });
338
- }
339
- );
340
-
341
- server.tool(
342
- "pause_file_transfer_batch",
343
- "Pause an active file transfer queue started or visible through AIPermission. Requires always_run permission for that server.",
344
- {
345
- batch_id: z.number().int().positive().describe("Batch id from list_file_transfer_batches or start_file_download."),
346
- },
347
- async ({ batch_id }) => {
348
- return jsonToolResult(() => apiPost(`/api/mcp/file-transfer-batches/${batch_id}/pause`, {}));
349
- }
350
- );
351
-
352
- server.tool(
353
- "resume_file_transfer_batch",
354
- "Resume a paused file transfer queue. Requires always_run permission for that server.",
355
- {
356
- batch_id: z.number().int().positive().describe("Batch id from list_file_transfer_batches or start_file_download."),
357
- },
358
- async ({ batch_id }) => {
359
- return jsonToolResult(() => apiPost(`/api/mcp/file-transfer-batches/${batch_id}/resume`, {}));
360
- }
361
- );
362
-
363
- server.tool(
364
- "cancel_file_transfer_batch",
365
- "Cancel a pending, running, or paused file transfer queue. Requires always_run permission for that server.",
83
+ "get_connector_action_request",
84
+ "Read one connector action request by id. Use this after call_connector_action returns approval_pending or running.",
366
85
  {
367
- batch_id: z.number().int().positive().describe("Batch id from list_file_transfer_batches or start_file_download."),
86
+ request_id: z.number().int().positive().describe("Request id returned by call_connector_action."),
368
87
  },
369
- async ({ batch_id }) => {
370
- return jsonToolResult(() => apiPost(`/api/mcp/file-transfer-batches/${batch_id}/cancel`, {}));
88
+ async ({ request_id }) => {
89
+ return jsonToolResult(() => apiGet(`/api/mcp/connector-action-requests/${request_id}`));
371
90
  }
372
91
  );
373
92
 
@@ -390,48 +109,12 @@ async function apiPost(path, body) {
390
109
  });
391
110
  }
392
111
 
393
- async function apiPostMultipart(path, fields, files) {
394
- const boundary = `aipermission-${Date.now()}-${Math.random().toString(16).slice(2)}`;
395
- return apiRequest(path, {
396
- method: "POST",
397
- headers: {
398
- "Content-Type": `multipart/form-data; boundary=${boundary}`,
399
- },
400
- body: Readable.from(multipartBody(boundary, fields, files)),
401
- duplex: "half",
402
- timeoutMs: apiTransferTimeoutMs,
403
- });
404
- }
405
-
406
- async function apiDownloadToFile(pathValue, destination, overwrite) {
407
- const response = await apiFetch(pathValue, {
408
- method: "GET",
409
- timeoutMs: apiTransferTimeoutMs,
410
- });
411
- if (!response.ok) {
412
- const text = await response.text();
413
- const data = parseResponseBody(text);
414
- throw new Error(data?.error || `aipermission API request failed with ${response.status}`);
415
- }
416
- let bytes = 0;
417
- const output = fs.createWriteStream(destination, {
418
- flags: overwrite ? "w" : "wx",
419
- mode: 0o600,
420
- });
421
- output.on("bytesWritten", (value) => {
422
- bytes = value;
423
- });
424
- await pipeline(Readable.fromWeb(response.body), output);
425
- const stat = await fsp.stat(destination);
426
- return { path: destination, bytes: stat.size || bytes };
427
- }
428
-
429
112
  async function apiRequest(path, options) {
430
113
  const response = await apiFetch(path, options);
431
114
  const text = await response.text();
432
115
  const data = parseResponseBody(text);
433
116
  if (!response.ok) {
434
- throw new Error(data?.error || `aipermission API request failed with ${response.status}`);
117
+ throw new Error(data?.error || `AIPermission API request failed with ${response.status}`);
435
118
  }
436
119
  return data;
437
120
  }
@@ -440,24 +123,22 @@ async function apiFetch(path, options) {
440
123
  if (!apiToken) {
441
124
  throw new Error("AIPERMISSION_API_TOKEN is required.");
442
125
  }
443
- const timeoutValue = options.timeoutMs || apiTimeoutMs;
444
- const timeout = Number.isFinite(timeoutValue) && timeoutValue > 0 ? timeoutValue : 60000;
126
+ const timeout = Number.isFinite(apiTimeoutMs) && apiTimeoutMs > 0 ? apiTimeoutMs : 60000;
445
127
  const controller = new AbortController();
446
128
  const timer = setTimeout(() => controller.abort(), timeout);
447
- const { timeoutMs: _timeoutMs, ...requestOptions } = options;
448
129
  let response;
449
130
  try {
450
131
  response = await fetch(`${apiUrl}${path}`, {
451
- ...requestOptions,
132
+ ...options,
452
133
  signal: controller.signal,
453
134
  headers: {
454
135
  Authorization: `Bearer ${apiToken}`,
455
- ...(requestOptions.headers || {}),
136
+ ...(options.headers || {}),
456
137
  },
457
138
  });
458
139
  } catch (error) {
459
140
  if (error?.name === "AbortError") {
460
- throw new Error(`aipermission API request timed out after ${timeout}ms`);
141
+ throw new Error(`AIPermission API request timed out after ${timeout}ms`);
461
142
  }
462
143
  throw error;
463
144
  } finally {
@@ -473,91 +154,6 @@ function parseResponseBody(text) {
473
154
  try {
474
155
  return JSON.parse(text);
475
156
  } catch {
476
- return { error: text.trim() || "Invalid non-JSON response from aipermission gateway." };
477
- }
478
- }
479
-
480
- async function resolveUploadFiles(localPaths) {
481
- const files = [];
482
- const seen = new Set();
483
- for (const rawPath of localPaths) {
484
- const resolved = expandHome(rawPath);
485
- if (seen.has(resolved)) {
486
- throw new Error(`duplicate local path: ${resolved}`);
487
- }
488
- seen.add(resolved);
489
- const stat = await fsp.stat(resolved).catch((error) => {
490
- throw new Error(`cannot read local file ${resolved}: ${error.message}`);
491
- });
492
- if (!stat.isFile()) {
493
- throw new Error(`local path is not a regular file: ${resolved}`);
494
- }
495
- files.push({
496
- field: "files",
497
- path: resolved,
498
- name: path.basename(resolved),
499
- size: stat.size,
500
- });
501
- }
502
- return files;
503
- }
504
-
505
- async function resolveLocalDestination(localPath, suggestedName, overwrite) {
506
- const resolved = expandHome(localPath);
507
- const existing = await fsp.stat(resolved).catch((error) => {
508
- if (error?.code === "ENOENT") return null;
509
- throw error;
510
- });
511
- if (existing?.isDirectory()) {
512
- return resolveLocalDestination(path.join(resolved, suggestedName), suggestedName, overwrite);
513
- }
514
- if (existing && !overwrite) {
515
- throw new Error(`local file already exists: ${resolved}`);
157
+ return { error: text };
516
158
  }
517
- const parent = path.dirname(resolved);
518
- const parentStat = await fsp.stat(parent).catch((error) => {
519
- throw new Error(`local directory does not exist: ${parent}: ${error.message}`);
520
- });
521
- if (!parentStat.isDirectory()) {
522
- throw new Error(`local parent path is not a directory: ${parent}`);
523
- }
524
- return resolved;
525
- }
526
-
527
- function suggestedDownloadFilename(batch) {
528
- if (Array.isArray(batch?.items) && batch.items.length === 1 && batch.items[0]?.file_name) {
529
- return safeLocalFilename(batch.items[0].file_name);
530
- }
531
- if (batch?.archive_name) {
532
- return safeLocalFilename(batch.archive_name);
533
- }
534
- return `aipermission-download-${batch?.id || Date.now()}.zip`;
535
- }
536
-
537
- function safeLocalFilename(value) {
538
- const base = path.basename(String(value || "").replaceAll("\0", ""));
539
- return base || "aipermission-download";
540
- }
541
-
542
- function expandHome(value) {
543
- const text = String(value || "").trim();
544
- if (text === "~") return os.homedir();
545
- if (text.startsWith("~/")) return path.join(os.homedir(), text.slice(2));
546
- return path.resolve(text);
547
- }
548
-
549
- async function* multipartBody(boundary, fields, files) {
550
- for (const [name, value] of Object.entries(fields)) {
551
- yield Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(name)}"\r\n\r\n${String(value)}\r\n`);
552
- }
553
- for (const file of files) {
554
- yield Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(file.field)}"; filename="${escapeMultipartName(file.name)}"\r\nContent-Type: application/octet-stream\r\n\r\n`);
555
- yield* fs.createReadStream(file.path);
556
- yield Buffer.from("\r\n");
557
- }
558
- yield Buffer.from(`--${boundary}--\r\n`);
559
- }
560
-
561
- function escapeMultipartName(value) {
562
- return String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
563
159
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
5
  "description": "Local-first MCP bridge for the aipermission gateway.",
6
- "license": "MIT",
6
+ "license": "AGPL-3.0-only",
7
7
  "type": "module",
8
8
  "homepage": "https://github.com/aipermission/aipermission/tree/main/packages/mcp#readme",
9
9
  "repository": {
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.1.13",
6
+ "version": "0.2.0",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.1.13",
11
+ "version": "0.2.0",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }