@priyanshumit/macos-terminal-mcp 0.4.0 → 0.5.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/README.md +3 -2
- package/dist/applescript.js +27 -1
- package/dist/applescript.js.map +1 -1
- package/dist/safety/confirm.js +16 -3
- package/dist/safety/confirm.js.map +1 -1
- package/dist/tools/execute.js +116 -5
- package/dist/tools/execute.js.map +1 -1
- package/dist/tools/new_tab.js +70 -0
- package/dist/tools/new_tab.js.map +1 -0
- package/dist/tools/register.js +2 -0
- package/dist/tools/register.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A local MCP server that lets AI agents inspect and drive your macOS Terminal.app
|
|
|
14
14
|
|
|
15
15
|
## What it does
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Twelve MCP tools across three categories:
|
|
18
18
|
|
|
19
19
|
### Terminal interaction
|
|
20
20
|
|
|
@@ -22,8 +22,9 @@ Eleven MCP tools across three categories:
|
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `terminal_list` | read | Enumerate every open Terminal.app tab with tty, title, busy state, and foreground processes. |
|
|
24
24
|
| `terminal_read` | read | Return the full buffer + scrollback of a specific tab, identified by tty. |
|
|
25
|
-
| `terminal_execute` | **write** | Type a command into a specific tab and press Enter.
|
|
25
|
+
| `terminal_execute` | **write** | Type a command into a specific tab and press Enter. Refuses if the target tab is busy unless `force=true`. `dry_run=true` returns the safety verdict without any side effects. |
|
|
26
26
|
| `terminal_clear` | **write** | Wipe scrollback of a specific tab via Cmd+K. Briefly steals focus. |
|
|
27
|
+
| `terminal_new_tab` | **write** | Open a new empty tab in Terminal.app and return its tty for follow-up calls. No dialog — low blast radius (user can close the tab). |
|
|
27
28
|
|
|
28
29
|
### Safety policy management
|
|
29
30
|
|
package/dist/applescript.js
CHANGED
|
@@ -3,11 +3,13 @@ export class OsascriptError extends Error {
|
|
|
3
3
|
stderr;
|
|
4
4
|
code;
|
|
5
5
|
timedOut;
|
|
6
|
-
|
|
6
|
+
aborted;
|
|
7
|
+
constructor(message, stderr, code, timedOut = false, aborted = false) {
|
|
7
8
|
super(message);
|
|
8
9
|
this.stderr = stderr;
|
|
9
10
|
this.code = code;
|
|
10
11
|
this.timedOut = timedOut;
|
|
12
|
+
this.aborted = aborted;
|
|
11
13
|
this.name = "OsascriptError";
|
|
12
14
|
}
|
|
13
15
|
}
|
|
@@ -15,12 +17,30 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
15
17
|
export function runJxa(script, options = {}) {
|
|
16
18
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17
19
|
return new Promise((resolve, reject) => {
|
|
20
|
+
// Fast-path: already-aborted signal — don't even spawn.
|
|
21
|
+
if (options.signal?.aborted) {
|
|
22
|
+
reject(new OsascriptError("osascript aborted before launch", "", -1, false, true));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
18
25
|
const proc = spawn("osascript", ["-l", "JavaScript"], {
|
|
19
26
|
stdio: ["pipe", "pipe", "pipe"],
|
|
20
27
|
});
|
|
21
28
|
let stdout = "";
|
|
22
29
|
let stderr = "";
|
|
23
30
|
let settled = false;
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
if (settled)
|
|
33
|
+
return;
|
|
34
|
+
settled = true;
|
|
35
|
+
try {
|
|
36
|
+
proc.kill("SIGKILL");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(new OsascriptError("osascript aborted", stderr.trim(), -1, false, true));
|
|
43
|
+
};
|
|
24
44
|
const timer = setTimeout(() => {
|
|
25
45
|
if (settled)
|
|
26
46
|
return;
|
|
@@ -31,8 +51,12 @@ export function runJxa(script, options = {}) {
|
|
|
31
51
|
catch {
|
|
32
52
|
/* ignore */
|
|
33
53
|
}
|
|
54
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
34
55
|
reject(new OsascriptError(`osascript timed out after ${timeoutMs}ms`, stderr.trim(), -1, true));
|
|
35
56
|
}, timeoutMs);
|
|
57
|
+
if (options.signal) {
|
|
58
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
}
|
|
36
60
|
proc.stdout.on("data", (chunk) => {
|
|
37
61
|
stdout += chunk.toString("utf8");
|
|
38
62
|
});
|
|
@@ -44,6 +68,7 @@ export function runJxa(script, options = {}) {
|
|
|
44
68
|
return;
|
|
45
69
|
settled = true;
|
|
46
70
|
clearTimeout(timer);
|
|
71
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
47
72
|
reject(err);
|
|
48
73
|
});
|
|
49
74
|
proc.on("close", (code) => {
|
|
@@ -51,6 +76,7 @@ export function runJxa(script, options = {}) {
|
|
|
51
76
|
return;
|
|
52
77
|
settled = true;
|
|
53
78
|
clearTimeout(timer);
|
|
79
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
54
80
|
if (code === 0) {
|
|
55
81
|
resolve(stdout.replace(/\n$/, ""));
|
|
56
82
|
}
|
package/dist/applescript.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"applescript.js","sourceRoot":"","sources":["../src/applescript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,OAAO,cAAe,SAAQ,KAAK;IAGrB;IACA;IACA;
|
|
1
|
+
{"version":3,"file":"applescript.js","sourceRoot":"","sources":["../src/applescript.ts"],"names":[],"mappings":"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"}
|
package/dist/safety/confirm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runJxa } from "../applescript.js";
|
|
1
|
+
import { OsascriptError, runJxa } from "../applescript.js";
|
|
2
2
|
export function isWriteToolsEnabled() {
|
|
3
3
|
return process.env.WRITE_TOOLS_ENABLED === "1";
|
|
4
4
|
}
|
|
@@ -32,8 +32,21 @@ app.includeStandardAdditions = true;
|
|
|
32
32
|
`;
|
|
33
33
|
// Outer osascript timeout is dialog timeout + 10s buffer so the dialog's own
|
|
34
34
|
// givingUpAfter has a chance to fire before the spawn-level kill engages.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
const result = await runJxa(script, {
|
|
37
|
+
timeoutMs: (timeoutSec + 10) * 1000,
|
|
38
|
+
signal: req.signal,
|
|
39
|
+
});
|
|
40
|
+
return result.trim() === "ALLOW";
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// Aborted via signal — treat as denial. Caller is responsible for its own
|
|
44
|
+
// out-of-band approval path (e.g. queue resolution); we just dismiss the dialog.
|
|
45
|
+
if (err instanceof OsascriptError && err.aborted) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
37
50
|
}
|
|
38
51
|
/**
|
|
39
52
|
* Strip C0 control characters (including newlines, tabs, CR) from AI-supplied
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/safety/confirm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/safety/confirm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,UAAU,mBAAmB;IACjC,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,KAAK,GAAG,CAAC;AACjD,CAAC;AAaD,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAmB;IACvD,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;IACxC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,IAAI,MAAM,CAAC;IACrC,MAAM,UAAU,GAAG,GAAG,CAAC,cAAc,IAAI,0BAA0B,CAAC;IACpE,MAAM,MAAM,GAAG;;;;;;QAMT,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;;oBAEf,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;yBACzC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;wBACrB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;qBACvB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;;yBAErB,UAAU;;;;uCAII,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;CAK3D,CAAC;IACA,6EAA6E;IAC7E,0EAA0E;IAC1E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE;YAClC,SAAS,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,GAAG,IAAI;YACnC,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0EAA0E;QAC1E,iFAAiF;QACjF,IAAI,GAAG,YAAY,cAAc,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YACjD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,2EAA2E;AAC3E,6EAA6E;AAC7E,8EAA8E;AAC9E,8OAA8O;AAC9O,MAAM,gBAAgB,GAAG,IAAI,MAAM,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;AAElE,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACxD,OAAO,CACL,GAAG,QAAQ,yEAAyE;QACpF,qEAAqE;QACrE,4EAA4E;QAC5E,0CAA0C,CAC3C,CAAC;AACJ,CAAC"}
|
package/dist/tools/execute.js
CHANGED
|
@@ -25,10 +25,30 @@ function safe(fn) { try { return fn(); } catch (e) { return null; } }
|
|
|
25
25
|
})(${JSON.stringify(tty)}, ${JSON.stringify(command)});
|
|
26
26
|
`;
|
|
27
27
|
}
|
|
28
|
+
function buildBusyCheckScript(tty) {
|
|
29
|
+
return `
|
|
30
|
+
function safe(fn) { try { return fn(); } catch (e) { return null; } }
|
|
31
|
+
(function checkBusy(targetTty) {
|
|
32
|
+
const terminal = Application("Terminal");
|
|
33
|
+
const wins = terminal.windows();
|
|
34
|
+
for (let wi = 0; wi < wins.length; wi++) {
|
|
35
|
+
const w = wins[wi];
|
|
36
|
+
const tabs = w.tabs();
|
|
37
|
+
for (let ti = 0; ti < tabs.length; ti++) {
|
|
38
|
+
const t = tabs[ti];
|
|
39
|
+
if (safe(function () { return t.tty(); }) === targetTty) {
|
|
40
|
+
return safe(function () { return t.busy(); }) ? "busy" : "idle";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return "missing";
|
|
45
|
+
})(${JSON.stringify(tty)});
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
28
48
|
function truncate(s, max) {
|
|
29
49
|
return s.length > max ? `${s.slice(0, max)}… [${s.length - max} chars elided]` : s;
|
|
30
50
|
}
|
|
31
|
-
export async function executeHandler({ tty, command }) {
|
|
51
|
+
export async function executeHandler({ tty, command, force = false, dry_run = false, }) {
|
|
32
52
|
if (!isWriteToolsEnabled()) {
|
|
33
53
|
return {
|
|
34
54
|
content: [{ type: "text", text: writeToolsDisabledMessage("terminal_execute") }],
|
|
@@ -37,6 +57,27 @@ export async function executeHandler({ tty, command }) {
|
|
|
37
57
|
}
|
|
38
58
|
const config = await loadSafetyConfig();
|
|
39
59
|
const verdict = evaluateCommand(command, config);
|
|
60
|
+
// dry_run short-circuits BEFORE any side effects (busy probe via JXA, audit
|
|
61
|
+
// log write, enqueue, dialog). Just compute and return what would happen.
|
|
62
|
+
if (dry_run) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
dry_run: true,
|
|
69
|
+
verdict: verdict.level,
|
|
70
|
+
matchedPattern: verdict.matchedPattern ?? null,
|
|
71
|
+
matchedDescription: verdict.matchedDescription ?? null,
|
|
72
|
+
wouldRefuse: verdict.level === "forbidden",
|
|
73
|
+
wouldPromptUser: verdict.level === "requires_approval",
|
|
74
|
+
wouldAutoRun: verdict.level === "safe",
|
|
75
|
+
note: "force/busy/permission checks are NOT evaluated in dry_run — only the safety policy verdict.",
|
|
76
|
+
}, null, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
40
81
|
if (verdict.level === "forbidden") {
|
|
41
82
|
const descPart = verdict.matchedDescription ? ` (${verdict.matchedDescription})` : "";
|
|
42
83
|
await appendAudit({
|
|
@@ -58,6 +99,54 @@ export async function executeHandler({ tty, command }) {
|
|
|
58
99
|
isError: true,
|
|
59
100
|
};
|
|
60
101
|
}
|
|
102
|
+
// Busy-tab check (#33). do script types into the foreground process's stdin
|
|
103
|
+
// when the tab is busy, which is rarely what callers want — fail fast unless
|
|
104
|
+
// force=true is set explicitly.
|
|
105
|
+
if (!force) {
|
|
106
|
+
let busyStatus;
|
|
107
|
+
try {
|
|
108
|
+
busyStatus = (await runJxa(buildBusyCheckScript(tty))).trim();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `terminal_execute failed to probe target tab busy state: ${message}`,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (busyStatus === "missing") {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `No Terminal.app tab found with tty ${tty}.` }],
|
|
125
|
+
isError: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (busyStatus === "busy") {
|
|
129
|
+
await appendAudit({
|
|
130
|
+
tool: "terminal_execute",
|
|
131
|
+
outcome: "refused",
|
|
132
|
+
tty,
|
|
133
|
+
command,
|
|
134
|
+
level: verdict.level,
|
|
135
|
+
matchedPattern: verdict.matchedPattern,
|
|
136
|
+
details: { reason: "target tab is busy" },
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `Refused: target tab ${tty} is busy with a running command. The command would be typed into the foreground process's stdin rather than executed. ` +
|
|
143
|
+
`Pass force=true if you genuinely want to send stdin, or wait for the running command to finish.`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
isError: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
61
150
|
let resolutionSource = "auto";
|
|
62
151
|
if (verdict.level === "requires_approval") {
|
|
63
152
|
const descPart = verdict.matchedDescription
|
|
@@ -71,11 +160,23 @@ export async function executeHandler({ tty, command }) {
|
|
|
71
160
|
matchedPattern: verdict.matchedPattern,
|
|
72
161
|
matchedDescription: verdict.matchedDescription,
|
|
73
162
|
});
|
|
74
|
-
|
|
163
|
+
// If the queue resolves out-of-band (via pending_approve / pending_deny),
|
|
164
|
+
// abort the dialog so it disappears from the user's screen instead of
|
|
165
|
+
// dangling (reviewer #4).
|
|
166
|
+
const dialogAbort = new AbortController();
|
|
167
|
+
const dialogPromise = confirmWithUser({
|
|
75
168
|
title: "macos-terminal-mcp · terminal_execute",
|
|
76
169
|
message: `Run in ${tty}:\n\n${sanitizeAiText(truncate(command, 800))}${descPart}\n\nQueue id: ${id}`,
|
|
77
|
-
|
|
170
|
+
signal: dialogAbort.signal,
|
|
171
|
+
});
|
|
172
|
+
void dialogPromise.then((allowed) => resolvePending(id, allowed, "dialog"), () => undefined);
|
|
78
173
|
const result = await promise;
|
|
174
|
+
if (result.source !== "dialog") {
|
|
175
|
+
// Queue resolved first — dismiss the still-open dialog.
|
|
176
|
+
dialogAbort.abort();
|
|
177
|
+
// Let the dialog promise settle (rejected with aborted) before continuing.
|
|
178
|
+
await dialogPromise.catch(() => undefined);
|
|
179
|
+
}
|
|
79
180
|
if (!result.approved) {
|
|
80
181
|
const reasonPart = result.reason ? ` (${result.reason})` : "";
|
|
81
182
|
await appendAudit({
|
|
@@ -105,6 +206,7 @@ export async function executeHandler({ tty, command }) {
|
|
|
105
206
|
const path = verdict.level === "safe"
|
|
106
207
|
? `auto-run (safe pattern: ${verdict.matchedPattern})`
|
|
107
208
|
: `approved via ${resolutionSource}`;
|
|
209
|
+
const forcedSuffix = force ? " [force=true]" : "";
|
|
108
210
|
await appendAudit({
|
|
109
211
|
tool: "terminal_execute",
|
|
110
212
|
outcome: "success",
|
|
@@ -113,12 +215,13 @@ export async function executeHandler({ tty, command }) {
|
|
|
113
215
|
level: verdict.level,
|
|
114
216
|
matchedPattern: verdict.matchedPattern,
|
|
115
217
|
source: verdict.level === "safe" ? "auto" : resolutionSource,
|
|
218
|
+
...(force ? { details: { force: true } } : {}),
|
|
116
219
|
});
|
|
117
220
|
return {
|
|
118
221
|
content: [
|
|
119
222
|
{
|
|
120
223
|
type: "text",
|
|
121
|
-
text: `Executed in ${tty} [${path}]: ${truncate(command, 200)}`,
|
|
224
|
+
text: `Executed in ${tty} [${path}${forcedSuffix}]: ${truncate(command, 200)}`,
|
|
122
225
|
},
|
|
123
226
|
],
|
|
124
227
|
};
|
|
@@ -145,13 +248,21 @@ export async function executeHandler({ tty, command }) {
|
|
|
145
248
|
}
|
|
146
249
|
export function register(server) {
|
|
147
250
|
server.registerTool("terminal_execute", {
|
|
148
|
-
description: 'Run a shell command in a specific Terminal.app tab identified by its tty (e.g. "/dev/ttys003"). Behaves as if the command was typed by the user and Enter was pressed — output appears in the tab. Three-tier safety: "safe" patterns auto-run, "requires_approval" patterns trigger a native confirmation dialog, "forbidden" patterns are refused outright. Requires WRITE_TOOLS_ENABLED=1.',
|
|
251
|
+
description: 'Run a shell command in a specific Terminal.app tab identified by its tty (e.g. "/dev/ttys003"). Behaves as if the command was typed by the user and Enter was pressed — output appears in the tab. Three-tier safety: "safe" patterns auto-run, "requires_approval" patterns trigger a native confirmation dialog (with parallel async approval via pending_*), "forbidden" patterns are refused outright. Before any of that, the target tab is checked for busy state (a running foreground command) — busy tabs reject by default unless force=true is set. dry_run=true short-circuits before any side effects and returns just the verdict. Requires WRITE_TOOLS_ENABLED=1.',
|
|
149
252
|
inputSchema: {
|
|
150
253
|
tty: z
|
|
151
254
|
.string()
|
|
152
255
|
.regex(/^\/dev\/ttys[0-9]+$/)
|
|
153
256
|
.describe('Target tab tty, e.g. "/dev/ttys003"'),
|
|
154
257
|
command: z.string().min(1).max(8192).describe("Shell command to run in the target tab"),
|
|
258
|
+
force: z
|
|
259
|
+
.boolean()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("If true, bypass the busy-tab check. The command will be typed into the foreground process's stdin rather than executed. Default: false."),
|
|
262
|
+
dry_run: z
|
|
263
|
+
.boolean()
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("If true, return the safety verdict and what WOULD happen without enqueueing, showing a dialog, or running anything. Useful for harnesses (or curious models) probing a call before allowing the real version."),
|
|
155
266
|
},
|
|
156
267
|
}, executeHandler);
|
|
157
268
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/tools/execute.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE7D,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,OAAe;IAC7D,OAAO;;;;;;;;;;;;;;;;;KAiBJ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;CACnD,CAAC;AACF,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,OAAO,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;
|
|
1
|
+
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/tools/execute.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { OsascriptError, runJxa } from "../applescript.js";
|
|
2
|
+
import { appendAudit } from "../safety/audit.js";
|
|
3
|
+
import { isWriteToolsEnabled, writeToolsDisabledMessage } from "../safety/confirm.js";
|
|
4
|
+
export const NEW_TAB_SCRIPT = `
|
|
5
|
+
function safe(fn) { try { return fn(); } catch (e) { return null; } }
|
|
6
|
+
(function newTab() {
|
|
7
|
+
const terminal = Application("Terminal");
|
|
8
|
+
terminal.activate();
|
|
9
|
+
const wins = terminal.windows();
|
|
10
|
+
// do script with empty command + no "in" → new window. With "in: front
|
|
11
|
+
// window" → new tab in that window. Prefer a new tab in the front window
|
|
12
|
+
// when one exists, otherwise let it create a new window.
|
|
13
|
+
let newTab;
|
|
14
|
+
if (wins.length > 0) {
|
|
15
|
+
newTab = terminal.doScript("", { in: wins[0] });
|
|
16
|
+
} else {
|
|
17
|
+
newTab = terminal.doScript("");
|
|
18
|
+
}
|
|
19
|
+
const tty = safe(function () { return newTab.tty(); }) || "";
|
|
20
|
+
let windowId = null;
|
|
21
|
+
try {
|
|
22
|
+
const after = terminal.windows();
|
|
23
|
+
for (let i = 0; i < after.length; i++) {
|
|
24
|
+
const ts = after[i].tabs();
|
|
25
|
+
for (let j = 0; j < ts.length; j++) {
|
|
26
|
+
if (safe(function () { return ts[j].tty(); }) === tty) {
|
|
27
|
+
windowId = safe(function () { return after[i].id(); });
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (windowId !== null) break;
|
|
32
|
+
}
|
|
33
|
+
} catch (e) { /* best-effort */ }
|
|
34
|
+
return JSON.stringify({ tty, windowId });
|
|
35
|
+
})();
|
|
36
|
+
`;
|
|
37
|
+
export async function newTabHandler() {
|
|
38
|
+
if (!isWriteToolsEnabled()) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: writeToolsDisabledMessage("terminal_new_tab") }],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const json = await runJxa(NEW_TAB_SCRIPT);
|
|
46
|
+
await appendAudit({ tool: "terminal_new_tab", outcome: "success" });
|
|
47
|
+
return { content: [{ type: "text", text: json }] };
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
+
const hint = err instanceof OsascriptError && /not authorized/i.test(err.stderr)
|
|
52
|
+
? "\nAutomation permission missing — System Settings → Privacy & Security → Automation."
|
|
53
|
+
: "";
|
|
54
|
+
await appendAudit({
|
|
55
|
+
tool: "terminal_new_tab",
|
|
56
|
+
outcome: "error",
|
|
57
|
+
errorMessage: message,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: `terminal_new_tab failed: ${message}${hint}` }],
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function register(server) {
|
|
66
|
+
server.registerTool("terminal_new_tab", {
|
|
67
|
+
description: "Open a new empty tab in Terminal.app (in the frontmost window, or a new window if none are open). Returns {tty, windowId} for the new tab. Use the returned tty in subsequent terminal_read / terminal_execute calls. NOTE: this tool does NOT execute any command in the new tab — to run a command, call terminal_execute against the returned tty. Requires WRITE_TOOLS_ENABLED=1 but does NOT pop a confirmation dialog (low blast radius — the user can close an unwanted tab).",
|
|
68
|
+
}, newTabHandler);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=new_tab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"new_tab.js","sourceRoot":"","sources":["../../src/tools/new_tab.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAEtF,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgC7B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAChF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QAC1C,MAAM,WAAW,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACpE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,GACR,GAAG,YAAY,cAAc,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YACjE,CAAC,CAAC,sFAAsF;YACxF,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,WAAW,CAAC;YAChB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,OAAO;YAChB,YAAY,EAAE,OAAO;SACtB,CAAC,CAAC;QACH,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,4BAA4B,OAAO,GAAG,IAAI,EAAE,EAAE,CAAC;YAC/E,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,MAAiB;IACxC,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,WAAW,EACT,sdAAsd;KACzd,EACD,aAAa,CACd,CAAC;AACJ,CAAC"}
|
package/dist/tools/register.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as clear from "./clear.js";
|
|
2
2
|
import * as execute from "./execute.js";
|
|
3
3
|
import * as list from "./list.js";
|
|
4
|
+
import * as newTab from "./new_tab.js";
|
|
4
5
|
import * as pending from "./pending.js";
|
|
5
6
|
import * as read from "./read.js";
|
|
6
7
|
import * as safety from "./safety.js";
|
|
@@ -9,6 +10,7 @@ export function registerAll(server) {
|
|
|
9
10
|
read.register(server);
|
|
10
11
|
execute.register(server);
|
|
11
12
|
clear.register(server);
|
|
13
|
+
newTab.register(server);
|
|
12
14
|
safety.register(server);
|
|
13
15
|
pending.register(server);
|
|
14
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,UAAU,WAAW,CAAC,MAAiB;IAC3C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC"}
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/tools/register.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,UAAU,WAAW,CAAC,MAAiB;IAC3C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@priyanshumit/macos-terminal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server exposing macOS Terminal.app to AI agents via AppleScript / JXA. List windows, read scrollback, execute commands, clear buffers — with three-tier safety patterns and confirmation dialogs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|