@aipermission/mcp 0.1.9 → 0.1.13

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
@@ -70,6 +70,10 @@ The generated MCP config contains a bearer token. Keep it private. For project-l
70
70
 
71
71
  `exec` is intended for non-interactive commands. The gateway closes stdin for MCP command bodies so stdin-reading commands cannot consume the internal shell wrapper. Use the web console for interactive work.
72
72
 
73
+ Command responses can include `policy_warnings` for common high-risk command
74
+ patterns. They are best-effort safety rails and do not replace token
75
+ permissions, approval review, or operator judgment.
76
+
73
77
  File transfer tools are intentionally conservative. MCP can list transfer
74
78
  metadata, browse remote directories, start remote download queues, save
75
79
  completed downloads to explicit local paths, upload explicit local files, and
@@ -98,11 +102,11 @@ Supported clients:
98
102
  - `gemini`: `GEMINI.md`
99
103
  - `custom`: prints portable Markdown to stdout
100
104
 
101
- These instructions teach the agent how to poll `approval_pending` and `running` requests, handle `stale` approvals by sending a fresh request, read live console output, write short reasons, use explicit file transfer paths, and avoid printing secrets. The default installer uses the operator instruction bundled in the npm package; `--source` accepts local file paths only and rejects HTTP(S) sources.
105
+ These instructions teach the agent how to poll `approval_pending` and `running` requests, handle `stale` approvals by sending a fresh request, read live console output, recover stuck persistent console sessions with `restart_console_session`, write short reasons, use explicit file transfer paths, and avoid printing secrets. The default installer uses the operator instruction bundled in the npm package; `--source` accepts local file paths only and rejects HTTP(S) sources.
102
106
 
103
107
  ## Security Boundary
104
108
 
105
- This package talks to a local AIPermission gateway. `AIPERMISSION_API_URL` must point to `localhost`, `127.0.0.1`, or `[::1]`; remote URLs are rejected before the bearer token is sent. Do not expose the gateway on LAN or the public internet, and do not use it as a shared DevOps service. Tokens grant access only to the servers and execution rules configured in the gateway UI. Token/server permissions may be temporary; expired grants are omitted from `list_servers` and no longer authorize command, console, or file-transfer tools.
109
+ This package talks to a local AIPermission gateway. `AIPERMISSION_API_URL` must point to `localhost`, `127.0.0.1`, or `[::1]`; remote URLs are rejected before the bearer token is sent. Do not expose the gateway on LAN or the public internet, and do not use it as a shared DevOps service. Tokens grant access only to the servers and execution rules configured in the gateway UI. Token/server permissions may be temporary; expired grants are omitted from `list_servers` and no longer authorize command, console, or file-transfer tools. `list_servers` is permission-scoped, not a live SSH health check; treat `exec` connection errors as the current reachability signal.
106
110
 
107
111
  ## License
108
112
 
@@ -27,9 +27,15 @@ If no server is visible, say that the current token has no accessible servers.
27
27
  If `expires_at` is present, treat access as temporary. Finish within that
28
28
  maintenance window or ask the operator to extend access.
29
29
 
30
+ `list_servers()` is permission-scoped, not a live health check. A visible server
31
+ may still be powered off, unreachable, reject SSH authentication, or require host
32
+ key review. Treat `exec` dial, timeout, authentication, and host-key errors as
33
+ the current reachability signal.
34
+
30
35
  ## Command Reasons
31
36
 
32
- Every `exec` call should include a short `reason`.
37
+ Every single-server `exec` call should include a short `reason`. Multi-server
38
+ `exec` calls with `server_ids` must include a short `reason`.
33
39
 
34
40
  Good reasons:
35
41
 
@@ -47,6 +53,30 @@ debug
47
53
  test
48
54
  ```
49
55
 
56
+ ## Bulk Command Flow
57
+
58
+ Use `exec(server_ids, command, reason)` when the operator asks for the same
59
+ non-interactive command on multiple servers, such as checking package update
60
+ state, uptime, disk usage, or a service status across a small fleet.
61
+
62
+ Before using multi-server `exec`:
63
+
64
+ 1. Call `list_servers()`.
65
+ 2. Choose only the requested target servers.
66
+ 3. Keep the command non-interactive and bounded.
67
+ 4. Use one clear reason that applies to every target.
68
+
69
+ After multi-server `exec` returns:
70
+
71
+ 1. Inspect every item.
72
+ 2. For each item with `request_id`, poll `get_request(request_id)` until terminal.
73
+ 3. If several `always_run` items are still running, use `read_console(server_ids, tail)` to inspect live output in one read-only call.
74
+ 4. Treat `blocked` or unauthorized items as skipped, not failed executions.
75
+ 5. Do not assume all targets started just because the top-level status is `accepted`.
76
+
77
+ Prefer individual `exec` calls when each server needs a different command,
78
+ different reason, or separate investigation path.
79
+
50
80
  ## Approval Flow
51
81
 
52
82
  `approval_pending` is not terminal.
@@ -72,6 +102,13 @@ stale
72
102
 
73
103
  If the request is `declined`, read `user_note` and follow the user's correction.
74
104
  If the request is `stale`, the approval context changed before execution; send a fresh `exec` request with the current command and reason instead of trying to reuse the old approval.
105
+ If the request is `error`, read the error text. SSH reachability, refused ports,
106
+ authentication failures, and host-key failures are current connection signals;
107
+ do not assume a listed server is live just because `list_servers` returned it.
108
+
109
+ If a response or request contains `policy_warnings`, mention the warning briefly
110
+ when it affects the next action. These warnings are best-effort safety rails, not
111
+ permission to skip human judgment.
75
112
 
76
113
  ## Running Flow
77
114
 
@@ -79,15 +116,16 @@ When `exec` or `get_request` returns `running`:
79
116
 
80
117
  1. If the server permission is `always_run`, call `read_console(server_id)` before sending another long-running command to the same server.
81
118
  2. Poll `get_request(request_id)` every 3-5 seconds.
82
- 3. Use `read_console(server_id)` between polls only when the token has `always_run` permission.
119
+ 3. Use `read_console(server_id)` between polls only when the token has `always_run` permission. For a multi-server command, use `read_console(server_ids, tail)` to inspect several running consoles at once.
83
120
  4. Do not start another long-running command on the same server until the active request reaches a terminal status, unless the user explicitly asks.
84
121
 
85
122
  If the request appears stuck for an unusually long time and `read_console`
86
- shows no useful progress, ask the operator before recovery. When the operator
87
- agrees, call `restart_console_session(server_id)`. This closes the
88
- gateway-owned persistent console session and the next `exec` opens a fresh SSH
89
- session. Treat any canceled request as inconclusive and re-run only the minimal
90
- safe inspection needed.
123
+ shows no useful progress, ask the operator before recovery unless they already
124
+ asked you to recover the session. When recovery is approved, call
125
+ `restart_console_session(server_id)`. This does not intentionally run a remote
126
+ shell command; it closes the gateway-owned persistent console session and the
127
+ next `exec` opens a fresh SSH session. Treat any canceled request as
128
+ inconclusive and re-run only the minimal safe inspection needed.
91
129
 
92
130
  ## Message Flow
93
131
 
package/dist/server.js CHANGED
@@ -25,7 +25,7 @@ const apiTransferTimeoutMs = Number.parseInt(process.env.AIPERMISSION_TRANSFER_T
25
25
 
26
26
  const server = new McpServer({
27
27
  name: "aipermission",
28
- version: "0.1.2",
28
+ version: "0.1.13",
29
29
  });
30
30
 
31
31
  server.tool(
@@ -39,35 +39,86 @@ server.tool(
39
39
 
40
40
  server.tool(
41
41
  "exec",
42
- "Execute a shell command on an allowed server 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.",
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.",
43
43
  {
44
- server_id: z.number().int().positive().describe("Server id from list_servers."),
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."),
45
46
  command: z.string().min(1).describe("Shell command to execute."),
46
- reason: z.string().optional().describe("Why this command is needed."),
47
+ reason: z.string().optional().describe("Why this command is needed. Required when using server_ids."),
47
48
  },
48
- async ({ server_id, command, reason }) => {
49
- return jsonToolResult(() => apiPost("/api/mcp/exec", {
50
- server_id,
51
- command,
52
- reason: reason || "",
53
- }));
49
+ async ({ server_id, server_ids, command, reason }) => {
50
+ 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
+ });
72
+ });
54
73
  }
55
74
  );
56
75
 
57
76
  server.tool(
58
77
  "read_console",
59
- "Read the latest persistent console transcript for an allowed server. Use this after a long-running exec returns running.",
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.",
60
79
  {
61
- server_id: z.number().int().positive().describe("Server id from list_servers."),
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."),
62
82
  tail: z.number().int().positive().max(100000).optional().describe("Maximum transcript characters to return."),
63
83
  },
64
- async ({ server_id, tail }) => {
65
- return jsonToolResult(() => {
66
- const params = new URLSearchParams({ server_id: String(server_id) });
67
- if (tail) {
68
- params.set("tail", String(tail));
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
+ };
69
120
  }
70
- return apiGet(`/api/mcp/console?${params.toString()}`);
121
+ return readOne(server_id);
71
122
  });
72
123
  }
73
124
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.13",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
5
  "description": "Local-first MCP bridge for the aipermission gateway.",
6
6
  "license": "MIT",
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.9",
6
+ "version": "0.1.13",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.1.9",
11
+ "version": "0.1.13",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }