@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 +233 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +427 -0
- package/dist/index.js.map +1 -0
- package/dist/tmux.d.ts +97 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +595 -0
- package/dist/tmux.js.map +1 -0
- package/package.json +52 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|