@towry/mcp 0.1.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 ADDED
@@ -0,0 +1,233 @@
1
+ # @towry/mcp
2
+
3
+ MCP (Model Context Protocol) server for Knowledge Graph document management and Tmux pane control.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @towry/mcp
9
+ # or
10
+ npm install @towry/mcp
11
+ ```
12
+
13
+ ## Environment Variables
14
+
15
+ | Variable | Description | Default |
16
+ |----------|-------------|---------|
17
+ | `KG_API_URL` | Knowledge Graph API base URL | `http://localhost:8361` |
18
+ | `KG_API_KEY` | API authentication key | `kg-dev-api-key` |
19
+ | `KG_API_TOKEN` | Alternative to KG_API_KEY | - |
20
+
21
+ ## Usage
22
+
23
+ ### As a CLI
24
+
25
+ ```bash
26
+ KG_API_URL=http://localhost:8361 pnpm dlx @towry/mcp
27
+ ```
28
+
29
+ ### With Claude Desktop
30
+
31
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "towry-mcp-kg": {
37
+ "command": "npx",
38
+ "args": ["@towry/mcp"],
39
+ "env": {
40
+ "KG_API_URL": "http://localhost:8361",
41
+ "KG_API_KEY": "your-api-key"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Available Tools
49
+
50
+ ### `kg_create_doc`
51
+
52
+ Create or update a document in the knowledge graph.
53
+
54
+ **Parameters:**
55
+ - `title` (string, required): Document title
56
+ - `body` (string, required): Document content
57
+ - `doc_type` (enum, required): `note` | `doc` | `task` | `plan` | `agent_session`
58
+ - `id` (string, optional): UUID to update existing doc
59
+ - `project_repo` (string, optional): Project repo (owner/repo)
60
+ - `tags` (string[], optional): Tags for categorization
61
+ - `status` (enum, optional): `pending` | `in_progress` | `done` | `blocked` (for tasks)
62
+ - `parent_id` (string, optional): Parent document ID for hierarchy
63
+
64
+ ### `kg_get_doc`
65
+
66
+ Get full document by ID.
67
+
68
+ **Parameters:**
69
+ - `id` (string, required): Document UUID
70
+
71
+ ### `kg_delete_doc`
72
+
73
+ Delete a document from the knowledge graph.
74
+
75
+ **Parameters:**
76
+ - `id` (string, required): Document UUID to delete
77
+
78
+ ### `kg_search`
79
+
80
+ Search documents with filters.
81
+
82
+ **Parameters:**
83
+ - `q` (string, optional): Search query
84
+ - `doc_type` (enum, optional): Filter by document type
85
+ - `project_repo` (string, optional): Filter by project repo
86
+ - `status` (enum, optional): Filter by task status
87
+ - `tag` (string, optional): Filter by tag
88
+ - `limit` (number, optional): Max results (1-100, default: 20)
89
+ - `offset` (number, optional): Pagination offset (default: 0)
90
+
91
+ ### `kg_semantic_search`
92
+
93
+ Semantic search using embeddings.
94
+
95
+ **Parameters:**
96
+ - `q` (string, required): Search query
97
+ - `embedding_keywords` (string[], optional): Keywords for embedding search
98
+ - `doc_type` (enum, optional): Filter by document type
99
+ - `project_repo` (string, optional): Filter by project repo
100
+ - `tag` (string, optional): Filter by tag
101
+ - `limit` (number, optional): Max results (1-20, default: 5)
102
+
103
+ ### `kg_status`
104
+
105
+ Get document statistics: total count, breakdown by type and repo.
106
+
107
+ **Parameters:** None
108
+
109
+ ## Document Types
110
+
111
+ | Type | Use Case |
112
+ |------|----------|
113
+ | `note` | Session-specific learnings, debugging discoveries (ephemeral) |
114
+ | `doc` | Curated reference docs meant to be maintained (stable) |
115
+ | `task` | Actionable items with status tracking |
116
+ | `plan` | Multi-step roadmaps with phases |
117
+ | `agent_session` | Chat session logs |
118
+
119
+ ## Tmux Tools
120
+
121
+ ### `tmux_list_panes`
122
+
123
+ List all tmux panes.
124
+
125
+ **Parameters:** None
126
+
127
+ ### `tmux_send`
128
+
129
+ Send keys to a tmux pane.
130
+
131
+ **Parameters:**
132
+ - `pane` (string, optional): Target pane ID. Defaults to `PI_MASTER_PANE` env if set
133
+ - `keys` (string, required): Keys to send
134
+ - `enter` (boolean, optional): Send Enter after (default: true)
135
+
136
+ ### `tmux_kill_pane`
137
+
138
+ Kill a tmux pane by ID.
139
+
140
+ **Parameters:**
141
+ - `pane` (string, required): Pane ID in `%N` format (e.g., `%886`)
142
+
143
+ ### `tmux_capture`
144
+
145
+ Capture the content of a tmux pane.
146
+
147
+ **Parameters:**
148
+ - `pane` (string, required): Target pane ID to capture
149
+ - `lines` (number, optional): Number of lines to capture from end (default: 10)
150
+ - `filter` (string, optional): Grep pattern (case-insensitive, extended regex)
151
+
152
+ ### `tmux_run`
153
+
154
+ Run a command in a new tmux pane with duplicate detection.
155
+
156
+ **Parameters:**
157
+ - `command` (string, required): Command to run
158
+ - `name` (string, optional): Unique identifier for duplicate detection
159
+ - `cwd` (string, optional): Working directory for the command
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ # Install dependencies
165
+ pnpm install
166
+
167
+ # Build
168
+ pnpm build
169
+
170
+ # Watch mode
171
+ pnpm dev
172
+
173
+ # Lint
174
+ pnpm lint
175
+
176
+ # Type check
177
+ pnpm typecheck
178
+ ```
179
+
180
+ ## Debugging with MCP Inspector
181
+
182
+ The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a browser-based tool for testing and debugging MCP servers.
183
+
184
+ ### Quick Start
185
+
186
+ ```bash
187
+ # Using the package script (recommended)
188
+ pnpm inspect
189
+
190
+ # Or manually with environment variables
191
+ pnpm dlx @modelcontextprotocol/inspector -e KG_API_URL=http://localhost:8361 -e KG_API_KEY=your-key node dist/index.js
192
+
193
+ # Debug in watch mode (rebuild first, then run inspector)
194
+ pnpm build && npx @modelcontextprotocol/inspector node dist/index.js
195
+ ```
196
+
197
+ ### Inspector Features
198
+
199
+ Once the inspector opens in your browser (default: http://localhost:6274):
200
+
201
+ 1. **Tools Tab**: List all available tools, view their schemas, and execute them with test parameters
202
+ 2. **Resources Tab**: Browse any resources exposed by the server
203
+ 3. **Prompts Tab**: Test prompt templates
204
+ 4. **Notifications Pane**: View real-time server notifications and logs
205
+
206
+ ### Development Workflow
207
+
208
+ ```bash
209
+ # Terminal 1: Watch and rebuild on changes
210
+ pnpm dev
211
+
212
+ # Terminal 2: Run inspector (restart after rebuilds)
213
+ pnpm inspect
214
+ ```
215
+
216
+ ### CLI Mode
217
+
218
+ For quick testing without the browser UI:
219
+
220
+ ```bash
221
+ pnpm dlx @modelcontextprotocol/inspector --cli node dist/index.js
222
+ ```
223
+
224
+ ### Debugging Tips
225
+
226
+ - **Check tool schemas**: Use the Tools tab to verify parameter types and descriptions
227
+ - **Test edge cases**: Try empty inputs, invalid types, missing required fields
228
+ - **Monitor errors**: Watch the Notifications pane for server-side errors
229
+ - **Verify API connectivity**: Test `kg_status` first to ensure the KG API is reachable
230
+
231
+ ## License
232
+
233
+ MIT
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @towry/mcp - Knowledge Graph & Tmux MCP Server
4
+ *
5
+ * Provides tools for:
6
+ * - Document management in a knowledge graph
7
+ * - Tmux pane management and control
8
+ *
9
+ * Environment Variables:
10
+ * - KG_API_URL: API base URL (default: http://localhost:8361)
11
+ * - KG_API_KEY or KG_API_TOKEN: API authentication key
12
+ *
13
+ * Keywords: mcp, knowledge-graph, document-management, tmux, ai-tools, llm-server
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG"}
package/dist/index.js ADDED
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @towry/mcp - Knowledge Graph & Tmux MCP Server
4
+ *
5
+ * Provides tools for:
6
+ * - Document management in a knowledge graph
7
+ * - Tmux pane management and control
8
+ *
9
+ * Environment Variables:
10
+ * - KG_API_URL: API base URL (default: http://localhost:8361)
11
+ * - KG_API_KEY or KG_API_TOKEN: API authentication key
12
+ *
13
+ * Keywords: mcp, knowledge-graph, document-management, tmux, ai-tools, llm-server
14
+ */
15
+ import { spawn } from "node:child_process";
16
+ import { z } from "zod";
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { TmuxCLI, getCurrentPaneInfo, shellQuote } from "./tmux.js";
20
+ // ============================================================================
21
+ // Knowledge Graph Types
22
+ // ============================================================================
23
+ const DocTypeEnum = z.enum(["note", "doc", "task", "plan", "agent_session"]);
24
+ const TaskStatusEnum = z.enum(["pending", "in_progress", "done", "blocked"]);
25
+ // ============================================================================
26
+ // Knowledge Graph API Client
27
+ // ============================================================================
28
+ function getKgBaseUrl() {
29
+ return process.env.KG_API_URL ?? "http://localhost:8361";
30
+ }
31
+ function getKgApiKey() {
32
+ return process.env.KG_API_KEY ?? process.env.KG_API_TOKEN ?? "kg-dev-api-key";
33
+ }
34
+ async function kgApiRequest(method, path, body) {
35
+ const url = `${getKgBaseUrl()}${path}`;
36
+ const headers = {
37
+ "X-API-Key": getKgApiKey(),
38
+ "Content-Type": "application/json",
39
+ };
40
+ const res = await fetch(url, {
41
+ method,
42
+ headers,
43
+ body: body ? JSON.stringify(body) : undefined,
44
+ });
45
+ if (!res.ok) {
46
+ const text = await res.text();
47
+ throw new Error(`API error ${res.status}: ${text}`);
48
+ }
49
+ return res.json();
50
+ }
51
+ // ============================================================================
52
+ // Tmux CLI Instance
53
+ // ============================================================================
54
+ const tmux = new TmuxCLI();
55
+ const DEFAULT_CWD = process.cwd();
56
+ // ============================================================================
57
+ // MCP Server
58
+ // ============================================================================
59
+ const server = new McpServer({
60
+ name: "towry-mcp",
61
+ version: "0.1.0",
62
+ });
63
+ // ============================================================================
64
+ // Knowledge Graph Tools
65
+ // ============================================================================
66
+ // kg_create_doc - Create or update a document
67
+ server.registerTool("kg_create_doc", {
68
+ description: "Create or update a document. Use 'note' for session learnings (ephemeral), 'doc' for stable reference docs, 'plan' for roadmaps, 'task' for actionable items. For insights from current chat, prefer kg_insight_save.",
69
+ inputSchema: {
70
+ title: z.string().describe("Document title"),
71
+ body: z.string().describe("Document content"),
72
+ doc_type: DocTypeEnum.describe("note: session learnings (ephemeral), doc: stable reference, task: actionable items, plan: roadmaps, agent_session: chat logs"),
73
+ id: z.string().optional().describe("UUID to update existing doc"),
74
+ project_repo: z.string().optional().describe("Project repo (owner/repo)"),
75
+ tags: z.array(z.string()).optional().describe("Tags for categorization"),
76
+ status: TaskStatusEnum.optional().describe("Task status (only for task type)"),
77
+ parent_id: z.string().optional().describe("Parent document ID for hierarchy"),
78
+ },
79
+ }, async (params) => {
80
+ const result = await kgApiRequest("POST", "/api/docs", {
81
+ ...params,
82
+ doc_type: params.doc_type ?? "note",
83
+ });
84
+ return {
85
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
86
+ };
87
+ });
88
+ // kg_get_doc - Get full document by ID
89
+ server.registerTool("kg_get_doc", {
90
+ description: "Get full document by ID. Use AFTER kg_search when you need the complete body (snippets are truncated).",
91
+ inputSchema: {
92
+ id: z.string().describe("Document UUID"),
93
+ },
94
+ }, async (params) => {
95
+ const result = await kgApiRequest("GET", `/api/docs/${params.id}`);
96
+ return {
97
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
98
+ };
99
+ });
100
+ // kg_delete_doc - Delete a document
101
+ server.registerTool("kg_delete_doc", {
102
+ description: "Delete a document from the knowledge graph.",
103
+ inputSchema: {
104
+ id: z.string().describe("Document UUID to delete"),
105
+ },
106
+ }, async (params) => {
107
+ const result = await kgApiRequest("DELETE", `/api/docs/${params.id}`);
108
+ return {
109
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
110
+ };
111
+ });
112
+ // kg_search - Search documents
113
+ server.registerTool("kg_search", {
114
+ description: "Find plans, tasks, or docs. Use doc_type='plan' for plans, tag='insight' for insights. Returns snippets - use kg_get_doc for full content.",
115
+ inputSchema: {
116
+ q: z.string().optional().describe("Search query (optional - omit to list all of doc_type)"),
117
+ doc_type: DocTypeEnum.optional().describe("Filter by type: 'plan' for plans, 'note' for insights/learnings"),
118
+ project_repo: z.string().optional().describe("Filter by project repo"),
119
+ status: TaskStatusEnum.optional().describe("Filter by task status"),
120
+ tag: z.string().optional().describe("Filter by tag"),
121
+ limit: z.number().int().min(1).max(100).default(20).optional(),
122
+ offset: z.number().int().min(0).default(0).optional(),
123
+ },
124
+ }, async (params) => {
125
+ const searchParams = new URLSearchParams();
126
+ if (params.q)
127
+ searchParams.set("q", params.q);
128
+ if (params.doc_type)
129
+ searchParams.set("doc_type", params.doc_type);
130
+ if (params.project_repo)
131
+ searchParams.set("project_repo", params.project_repo);
132
+ if (params.status)
133
+ searchParams.set("status", params.status);
134
+ if (params.tag)
135
+ searchParams.set("tag", params.tag);
136
+ if (params.limit)
137
+ searchParams.set("limit", String(params.limit));
138
+ if (params.offset)
139
+ searchParams.set("offset", String(params.offset));
140
+ const result = await kgApiRequest("GET", `/api/docs/search?${searchParams.toString()}`);
141
+ return {
142
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
143
+ };
144
+ });
145
+ // kg_semantic_search - Semantic search using embeddings
146
+ server.registerTool("kg_semantic_search", {
147
+ description: "Semantic search using embeddings in the knowledge graph.",
148
+ inputSchema: {
149
+ q: z.string().describe("Search query (required)"),
150
+ embedding_keywords: z
151
+ .array(z.string())
152
+ .optional()
153
+ .describe("Keywords for embedding search. If provided, searches with these keywords. If not, fetches latest 2 docs."),
154
+ doc_type: DocTypeEnum.optional().describe("Filter by document type"),
155
+ project_repo: z.string().optional().describe("Filter by project repo"),
156
+ tag: z.string().optional().describe("Filter by tag"),
157
+ limit: z.number().int().min(1).max(20).default(5).optional(),
158
+ },
159
+ }, async (params) => {
160
+ const searchParams = new URLSearchParams();
161
+ const searchQuery = params.embedding_keywords && params.embedding_keywords.length > 0
162
+ ? params.embedding_keywords.join(" ")
163
+ : params.q;
164
+ searchParams.set("q", searchQuery);
165
+ if (params.doc_type)
166
+ searchParams.set("doc_type", params.doc_type);
167
+ if (params.project_repo)
168
+ searchParams.set("project_repo", params.project_repo);
169
+ if (params.tag)
170
+ searchParams.set("tag", params.tag);
171
+ if (params.limit)
172
+ searchParams.set("limit", String(params.limit));
173
+ const result = await kgApiRequest("GET", `/api/docs/semantic?${searchParams.toString()}`);
174
+ return {
175
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
176
+ };
177
+ });
178
+ // kg_status - Get document statistics
179
+ server.registerTool("kg_status", {
180
+ description: "Get document statistics: total count, breakdown by type (plan/task/doc/note) and by repo. Use to check what's stored before searching.",
181
+ inputSchema: {},
182
+ }, async () => {
183
+ const result = await kgApiRequest("GET", "/api/docs/status");
184
+ return {
185
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
186
+ };
187
+ });
188
+ // ============================================================================
189
+ // Tmux Tools
190
+ // ============================================================================
191
+ // tmux_list_panes - List all tmux panes
192
+ server.registerTool("tmux_list_panes", {
193
+ description: "List all tmux panes",
194
+ inputSchema: {},
195
+ }, async () => {
196
+ try {
197
+ const panes = await tmux.listTmuxPanes();
198
+ // Filter out current pane to prevent self-operations
199
+ const currentPane = process.env.TMUX_PANE || (await getCurrentPaneInfo()).id;
200
+ const filtered = currentPane ? panes.filter((p) => p.id !== currentPane) : panes;
201
+ return {
202
+ content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }],
203
+ };
204
+ }
205
+ catch (err) {
206
+ return {
207
+ content: [{ type: "text", text: String(err) }],
208
+ isError: true,
209
+ };
210
+ }
211
+ });
212
+ // tmux_send - Send keys to a pane
213
+ server.registerTool("tmux_send", {
214
+ description: "Send keys to a tmux pane. If PI_MASTER_PANE env is set and pane is omitted, sends to master.",
215
+ inputSchema: {
216
+ pane: z.string().optional().describe("Target pane id. Defaults to PI_MASTER_PANE if set."),
217
+ keys: z.string().describe("Keys to send"),
218
+ enter: z.boolean().default(true).optional().describe("Send Enter after"),
219
+ },
220
+ }, async (params) => {
221
+ const targetPane = params.pane || process.env.PI_MASTER_PANE;
222
+ if (!targetPane) {
223
+ return {
224
+ content: [{ type: "text", text: "pane is required (no PI_MASTER_PANE set)" }],
225
+ isError: true,
226
+ };
227
+ }
228
+ const currentPane = process.env.TMUX_PANE;
229
+ if (currentPane && targetPane === currentPane) {
230
+ return {
231
+ content: [{ type: "text", text: "cannot send keys to current pane" }],
232
+ isError: true,
233
+ };
234
+ }
235
+ try {
236
+ const paneInfo = await getCurrentPaneInfo();
237
+ const signature = paneInfo.id
238
+ ? ` [from ${paneInfo.id}${paneInfo.title ? `: ${paneInfo.title}` : ""}]`
239
+ : "";
240
+ const keysWithSig = params.keys + signature;
241
+ await tmux.sendTmuxKeys(keysWithSig, { pane: targetPane, enter: params.enter ?? true });
242
+ return {
243
+ content: [{ type: "text", text: "sent" }],
244
+ };
245
+ }
246
+ catch (err) {
247
+ return {
248
+ content: [{ type: "text", text: String(err) }],
249
+ isError: true,
250
+ };
251
+ }
252
+ });
253
+ // tmux_kill_pane - Kill a pane
254
+ server.registerTool("tmux_kill_pane", {
255
+ description: "Kill a tmux pane by index (cannot close the last page)",
256
+ inputSchema: {
257
+ pane: z
258
+ .string()
259
+ .describe("Pane ID in %N format (e.g., %886). Do NOT use session:window.pane format - indices shift when panes are killed."),
260
+ },
261
+ }, async (params) => {
262
+ // Reject volatile formatted IDs
263
+ if (params.pane.includes(":") || params.pane.includes(".")) {
264
+ return {
265
+ content: [
266
+ {
267
+ type: "text",
268
+ text: `Invalid pane ID format: "${params.pane}". Use stable %N format (e.g., %886), not session:window.pane format which shifts when panes are killed.`,
269
+ },
270
+ ],
271
+ isError: true,
272
+ };
273
+ }
274
+ const normalize = (id) => (id.startsWith("%") ? id : `%${id}`);
275
+ const inputNorm = normalize(params.pane);
276
+ // Safety: prevent killing current pane
277
+ const envPane = process.env.TMUX_PANE;
278
+ if (envPane && inputNorm === normalize(envPane)) {
279
+ return {
280
+ content: [{ type: "text", text: "cannot kill current pane (self)" }],
281
+ isError: true,
282
+ };
283
+ }
284
+ if (!envPane) {
285
+ const currentInfo = await getCurrentPaneInfo();
286
+ if (currentInfo.id && inputNorm === normalize(currentInfo.id)) {
287
+ return {
288
+ content: [{ type: "text", text: "cannot kill current pane (self)" }],
289
+ isError: true,
290
+ };
291
+ }
292
+ }
293
+ try {
294
+ await tmux.killTmuxPane(inputNorm);
295
+ return {
296
+ content: [{ type: "text", text: "killed" }],
297
+ };
298
+ }
299
+ catch (err) {
300
+ return {
301
+ content: [{ type: "text", text: String(err) }],
302
+ isError: true,
303
+ };
304
+ }
305
+ });
306
+ // tmux_capture - Capture pane content
307
+ server.registerTool("tmux_capture", {
308
+ description: "Capture the content of a tmux pane. Defaults to last 10 lines. Use filter for grep with context. Do not use this for polling.",
309
+ inputSchema: {
310
+ pane: z.string().describe("Target pane id to capture"),
311
+ lines: z.number().optional().describe("Number of lines to capture from end (default: 10)"),
312
+ filter: z
313
+ .string()
314
+ .optional()
315
+ .describe("Grep pattern (case-insensitive, extended regex). Use | for OR: 'error|fail|warning'"),
316
+ },
317
+ }, async (params) => {
318
+ const currentPane = process.env.TMUX_PANE;
319
+ if (currentPane && params.pane === currentPane) {
320
+ return {
321
+ content: [{ type: "text", text: "cannot capture current pane" }],
322
+ isError: true,
323
+ };
324
+ }
325
+ try {
326
+ let output = await tmux.captureTmuxPane({ pane: params.pane, lines: params.lines ?? 10 });
327
+ if (params.filter?.trim()) {
328
+ const grepProc = spawn("grep", ["-i", "-E", "-C3", params.filter.trim()], {
329
+ stdio: ["pipe", "pipe", "pipe"],
330
+ });
331
+ grepProc.stdin.write(output);
332
+ grepProc.stdin.end();
333
+ output = await new Promise((resolve) => {
334
+ let out = "";
335
+ grepProc.stdout.on("data", (d) => (out += d.toString()));
336
+ grepProc.on("close", () => resolve(out.trim()));
337
+ });
338
+ }
339
+ return {
340
+ content: [{ type: "text", text: output || "(no matching content)" }],
341
+ };
342
+ }
343
+ catch (err) {
344
+ return {
345
+ content: [{ type: "text", text: String(err) }],
346
+ isError: true,
347
+ };
348
+ }
349
+ });
350
+ // tmux_run - Run command in new pane
351
+ server.registerTool("tmux_run", {
352
+ description: "Run a command in a new tmux pane. Checks for existing panes with same name to prevent duplicates (e.g., port conflicts).",
353
+ inputSchema: {
354
+ command: z.string().describe("Command to run"),
355
+ name: z
356
+ .string()
357
+ .optional()
358
+ .describe("Unique identifier for duplicate detection. If omitted, derived from command."),
359
+ cwd: z
360
+ .string()
361
+ .optional()
362
+ .describe("Working directory for the command. Defaults to current project cwd."),
363
+ },
364
+ }, async (params) => {
365
+ if (!params.command?.trim()) {
366
+ return {
367
+ content: [{ type: "text", text: "command is required" }],
368
+ isError: true,
369
+ };
370
+ }
371
+ const workingDir = params.cwd || DEFAULT_CWD;
372
+ // Derive title
373
+ const cmdSlug = params.command
374
+ .trim()
375
+ .slice(0, 40)
376
+ .replace(/[^a-zA-Z0-9 _\-.]/g, "")
377
+ .trim();
378
+ const title = params.name?.trim()
379
+ ? `pi-run:${cmdSlug}:${params.name.trim()}`
380
+ : `pi-run:${cmdSlug}`;
381
+ try {
382
+ // Check for existing pane with same title
383
+ const panes = await tmux.listTmuxPanes();
384
+ const existing = panes.find((pane) => pane.title === title);
385
+ if (existing) {
386
+ return {
387
+ content: [
388
+ {
389
+ type: "text",
390
+ text: `pane "${title}" already exists: ${existing.id}. Kill it first to restart.`,
391
+ },
392
+ ],
393
+ isError: true,
394
+ };
395
+ }
396
+ // Build command with title setting
397
+ const startCommand = `cd ${shellQuote(workingDir)} && tmux set-option -p allow-set-title off && tmux select-pane -T ${shellQuote(title)} && ${params.command}`;
398
+ const paneId = await tmux.launchTmuxPane(startCommand, { vertical: true, size: 50 });
399
+ if (!paneId) {
400
+ return {
401
+ content: [{ type: "text", text: "launch failed" }],
402
+ isError: true,
403
+ };
404
+ }
405
+ return {
406
+ content: [{ type: "text", text: `started in pane ${paneId}. title: ${title}` }],
407
+ };
408
+ }
409
+ catch (err) {
410
+ return {
411
+ content: [{ type: "text", text: String(err) }],
412
+ isError: true,
413
+ };
414
+ }
415
+ });
416
+ // ============================================================================
417
+ // Start Server
418
+ // ============================================================================
419
+ async function main() {
420
+ const transport = new StdioServerTransport();
421
+ await server.connect(transport);
422
+ }
423
+ main().catch((error) => {
424
+ console.error("Server error:", error);
425
+ process.exit(1);
426
+ });
427
+ //# sourceMappingURL=index.js.map