happy-mcp-server 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -0
- package/dist/server.js +16 -12
- package/dist/tools/answer_question.js +2 -6
- package/dist/tools/approve_permission.js +2 -10
- package/dist/tools/deny_permission.js +2 -7
- package/dist/tools/get_session.js +2 -7
- package/dist/tools/interrupt_session.js +2 -4
- package/dist/tools/list_computers.js +2 -1
- package/dist/tools/list_sessions.js +2 -8
- package/dist/tools/schemas.d.ts +86 -0
- package/dist/tools/schemas.js +85 -0
- package/dist/tools/send_message.js +2 -11
- package/dist/tools/start_session.js +6 -7
- package/dist/tools/stop_session.d.ts +4 -0
- package/dist/tools/stop_session.js +27 -0
- package/dist/tools/watch_session.js +2 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,171 @@ Environment variables customize server behavior:
|
|
|
92
92
|
| `HAPPY_MCP_SESSION_CACHE_TTL` | `300` | Session cache TTL in seconds |
|
|
93
93
|
| `HAPPY_MCP_ENABLE_START` | `true` | Set to `false` to disable the `start_session` tool. |
|
|
94
94
|
|
|
95
|
+
## Available Tools
|
|
96
|
+
|
|
97
|
+
When authenticated, happy-mcp-server exposes 11 MCP tools grouped into three categories. When unauthenticated, only the `authentication_status` tool is available to initiate QR-based pairing.
|
|
98
|
+
|
|
99
|
+
### Tool Summary
|
|
100
|
+
|
|
101
|
+
| Tool | Category | Description |
|
|
102
|
+
|------|----------|-------------|
|
|
103
|
+
| [`list_computers`](#list_computers) | Discovery | List available computers filtered by `HAPPY_MCP_COMPUTERS` |
|
|
104
|
+
| [`list_sessions`](#list_sessions) | Discovery | List active sessions with status and pending permissions |
|
|
105
|
+
| [`get_session`](#get_session) | Discovery | Get detailed info and recent messages for a session |
|
|
106
|
+
| [`watch_session`](#watch_session) | Discovery | Wait for session state changes (idle, permission pending) |
|
|
107
|
+
| [`send_message`](#send_message) | Session Control | Send a message to a session; optionally change permission mode |
|
|
108
|
+
| [`start_session`](#start_session) | Session Control | Start a new session on a remote machine |
|
|
109
|
+
| [`stop_session`](#stop_session) | Session Control | Stop and archive a running session |
|
|
110
|
+
| [`interrupt_session`](#interrupt_session) | Session Control | Abort current activity without terminating the session |
|
|
111
|
+
| [`approve_permission`](#approve_permission) | Permission Management | Approve a pending permission request |
|
|
112
|
+
| [`deny_permission`](#deny_permission) | Permission Management | Deny a pending permission request |
|
|
113
|
+
| [`answer_question`](#answer_question) | Permission Management | Answer an `AskUserQuestion` prompt from a session |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Discovery Tools
|
|
118
|
+
|
|
119
|
+
#### `list_computers`
|
|
120
|
+
|
|
121
|
+
List available computers filtered by `HAPPY_MCP_COMPUTERS`. Shows hostname, online status, and active session count. Use before `start_session` to find available machines.
|
|
122
|
+
|
|
123
|
+
No parameters.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
#### `list_sessions`
|
|
128
|
+
|
|
129
|
+
List all active Happy Coder sessions. Shows session ID, project path, hostname, status (`active`/`idle`/`waiting_permission`), and pending permissions.
|
|
130
|
+
|
|
131
|
+
Results are pre-filtered by `HAPPY_MCP_COMPUTERS` and `HAPPY_MCP_PROJECT_PATHS` unless those variables are set to `*`.
|
|
132
|
+
|
|
133
|
+
| Parameter | Type | Required | Default | Description |
|
|
134
|
+
|-----------|------|----------|---------|-------------|
|
|
135
|
+
| `filter` | object | No | — | Filter criteria |
|
|
136
|
+
| `filter.status` | enum | No | — | Filter by session status: `active`, `idle`, or `waiting_permission` |
|
|
137
|
+
| `filter.computer` | string | No | — | Filter by computer hostname |
|
|
138
|
+
| `filter.projectPath` | string | No | — | Filter by project path prefix |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
#### `get_session`
|
|
143
|
+
|
|
144
|
+
Get detailed information about a specific session including recent messages, metadata, and pending permissions.
|
|
145
|
+
|
|
146
|
+
| Parameter | Type | Required | Default | Description |
|
|
147
|
+
|-----------|------|----------|---------|-------------|
|
|
148
|
+
| `sessionId` | string | Yes | — | Session ID (from `list_sessions`) |
|
|
149
|
+
| `includeMessages` | boolean | No | `true` | Whether to include recent messages |
|
|
150
|
+
| `lastN` | number | No | `20` | Number of recent messages to return |
|
|
151
|
+
| `after` | string | No | — | ISO 8601 timestamp — only return messages after this time |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
#### `watch_session`
|
|
156
|
+
|
|
157
|
+
Watch one or more sessions for state changes. Returns immediately if any session is idle or has pending permissions. Otherwise waits up to 5 minutes for a state change. If sessions are still active when the timeout is reached, returns current status — call again to continue watching.
|
|
158
|
+
|
|
159
|
+
| Parameter | Type | Required | Default | Description |
|
|
160
|
+
|-----------|------|----------|---------|-------------|
|
|
161
|
+
| `sessionIds` | string[] | Yes | — | One or more session IDs to watch |
|
|
162
|
+
| `timeoutSeconds` | number | No | `300` | Maximum wait time in seconds |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### Session Control Tools
|
|
167
|
+
|
|
168
|
+
#### `send_message`
|
|
169
|
+
|
|
170
|
+
Send a message to a Happy Coder session. The message appears as a user message in the session. If the session is actively generating, the message is queued and processed when the session is ready. Can optionally change the permission mode.
|
|
171
|
+
|
|
172
|
+
| Parameter | Type | Required | Default | Description |
|
|
173
|
+
|-----------|------|----------|---------|-------------|
|
|
174
|
+
| `sessionId` | string | Yes | — | Session ID to send the message to |
|
|
175
|
+
| `message` | string | Yes | — | Message text to send |
|
|
176
|
+
| `meta` | object | No | — | Optional metadata |
|
|
177
|
+
| `meta.permissionMode` | enum | No | — | Permission mode: `default`, `acceptEdits`, `bypassPermissions`, `plan`, `read-only`, `safe-yolo`, `yolo` |
|
|
178
|
+
| `meta.allowedTools` | string[] | No | — | Tools to allow |
|
|
179
|
+
| `meta.disallowedTools` | string[] | No | — | Tools to disallow |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
#### `start_session`
|
|
184
|
+
|
|
185
|
+
Start a new Happy Coder session on a remote machine. Requires the machine to be online. Use `list_computers` to find available machines.
|
|
186
|
+
|
|
187
|
+
> Disabled when `HAPPY_MCP_ENABLE_START=false`.
|
|
188
|
+
|
|
189
|
+
| Parameter | Type | Required | Default | Description |
|
|
190
|
+
|-----------|------|----------|---------|-------------|
|
|
191
|
+
| `computer` | string | Yes | — | Machine ID (from `list_computers`) |
|
|
192
|
+
| `projectPath` | string | Yes | — | Working directory for the new session |
|
|
193
|
+
| `initialMessage` | string | No | — | Message to send after the session starts |
|
|
194
|
+
| `permissionMode` | enum | No | — | Permission mode: `default`, `acceptEdits`, `bypassPermissions`, `plan`, `read-only`, `safe-yolo`, `yolo` |
|
|
195
|
+
| `agent` | enum | No | `claude` | AI agent to use: `claude`, `codex`, or `gemini` |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
#### `stop_session`
|
|
200
|
+
|
|
201
|
+
Stop and archive a running Happy Coder session. Gracefully terminates the CLI process — the session becomes inactive but its message history is preserved. Use this when you are done with a session and want to free up resources.
|
|
202
|
+
|
|
203
|
+
| Parameter | Type | Required | Default | Description |
|
|
204
|
+
|-----------|------|----------|---------|-------------|
|
|
205
|
+
| `sessionId` | string | Yes | — | Session ID to stop |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
#### `interrupt_session`
|
|
210
|
+
|
|
211
|
+
Interrupt a running Claude Code session to stop its current activity. Sends an abort signal equivalent to pressing Escape — the session stays alive and can accept new messages afterward. Use this when a session is actively generating output and you need it to stop.
|
|
212
|
+
|
|
213
|
+
| Parameter | Type | Required | Default | Description |
|
|
214
|
+
|-----------|------|----------|---------|-------------|
|
|
215
|
+
| `sessionId` | string | Yes | — | Session ID to interrupt |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Permission Management Tools
|
|
220
|
+
|
|
221
|
+
#### `approve_permission`
|
|
222
|
+
|
|
223
|
+
Approve a pending permission request in a session. Permission requests appear in `get_session` and `watch_session` results.
|
|
224
|
+
|
|
225
|
+
| Parameter | Type | Required | Default | Description |
|
|
226
|
+
|-----------|------|----------|---------|-------------|
|
|
227
|
+
| `sessionId` | string | Yes | — | Session ID |
|
|
228
|
+
| `requestId` | string | Yes | — | Permission request ID to approve, or `"all_pending"` to approve all |
|
|
229
|
+
| `mode` | enum | No | — | Permission mode to set after approval: `default`, `acceptEdits`, `bypassPermissions`, `plan`, `read-only`, `safe-yolo`, `yolo` |
|
|
230
|
+
| `allowTools` | string[] | No | — | List of tools to allow |
|
|
231
|
+
| `decision` | enum | No | `approved` | Approval decision: `approved` or `approved_for_session` |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
#### `deny_permission`
|
|
236
|
+
|
|
237
|
+
Deny a pending permission request in a session. Can also abort the entire session.
|
|
238
|
+
|
|
239
|
+
| Parameter | Type | Required | Default | Description |
|
|
240
|
+
|-----------|------|----------|---------|-------------|
|
|
241
|
+
| `sessionId` | string | Yes | — | Session ID |
|
|
242
|
+
| `requestId` | string | Yes | — | Permission request ID to deny |
|
|
243
|
+
| `reason` | string | No | — | Feedback text explaining why the request was denied |
|
|
244
|
+
| `decision` | enum | No | `denied` | Denial type: `denied` or `abort` |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
#### `answer_question`
|
|
249
|
+
|
|
250
|
+
Answer a question (`AskUserQuestion`) from a session. Approves the pending permission and sends a user message with the answer.
|
|
251
|
+
|
|
252
|
+
| Parameter | Type | Required | Default | Description |
|
|
253
|
+
|-----------|------|----------|---------|-------------|
|
|
254
|
+
| `sessionId` | string | Yes | — | Session ID |
|
|
255
|
+
| `requestId` | string | Yes | — | Question request ID from pending permissions |
|
|
256
|
+
| `answers` | object | Yes | — | Map of question header to selected answer |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
95
260
|
## HTTP Transport
|
|
96
261
|
|
|
97
262
|
When running `happy-mcp-server serve`, the server exposes MCP over HTTP instead of stdio. This mode is useful for custom clients, testing, or programmatic access.
|
package/dist/server.js
CHANGED
|
@@ -9,6 +9,8 @@ import { registerDenyPermission } from './tools/deny_permission.js';
|
|
|
9
9
|
import { registerAnswerQuestion } from './tools/answer_question.js';
|
|
10
10
|
import { registerStartSession } from './tools/start_session.js';
|
|
11
11
|
import { registerInterruptSession } from './tools/interrupt_session.js';
|
|
12
|
+
import { registerStopSession } from './tools/stop_session.js';
|
|
13
|
+
import { listComputersSchema, listSessionsSchema, getSessionSchema, watchSessionSchema, sendMessageSchema, approvePermissionSchema, denyPermissionSchema, interruptSessionSchema, answerQuestionSchema, startSessionSchema, stopSessionSchema, } from './tools/schemas.js';
|
|
12
14
|
const AUTH_ERROR = {
|
|
13
15
|
isError: true,
|
|
14
16
|
content: [{
|
|
@@ -23,15 +25,16 @@ const AUTH_RETRY = {
|
|
|
23
25
|
}],
|
|
24
26
|
};
|
|
25
27
|
const TOOL_STUBS = [
|
|
26
|
-
['list_computers', 'List available computers filtered by HAPPY_MCP_COMPUTERS.'],
|
|
27
|
-
['list_sessions', 'List all active Happy Coder sessions.'],
|
|
28
|
-
['get_session', 'Get detailed state of a specific session.'],
|
|
29
|
-
['watch_session', 'Watch sessions for real-time updates.'],
|
|
30
|
-
['send_message', 'Send a message to a session.'],
|
|
31
|
-
['approve_permission', 'Approve a pending permission request.'],
|
|
32
|
-
['deny_permission', 'Deny a pending permission request.'],
|
|
33
|
-
['interrupt_session', 'Interrupt a running session to stop its current activity.'],
|
|
34
|
-
['
|
|
28
|
+
['list_computers', 'List available computers filtered by HAPPY_MCP_COMPUTERS.', listComputersSchema],
|
|
29
|
+
['list_sessions', 'List all active Happy Coder sessions.', listSessionsSchema],
|
|
30
|
+
['get_session', 'Get detailed state of a specific session.', getSessionSchema],
|
|
31
|
+
['watch_session', 'Watch sessions for real-time updates.', watchSessionSchema],
|
|
32
|
+
['send_message', 'Send a message to a session.', sendMessageSchema],
|
|
33
|
+
['approve_permission', 'Approve a pending permission request.', approvePermissionSchema],
|
|
34
|
+
['deny_permission', 'Deny a pending permission request.', denyPermissionSchema],
|
|
35
|
+
['interrupt_session', 'Interrupt a running session to stop its current activity.', interruptSessionSchema],
|
|
36
|
+
['stop_session', 'Stop and archive a running session.', stopSessionSchema],
|
|
37
|
+
['answer_question', 'Answer a question from a session.', answerQuestionSchema],
|
|
35
38
|
];
|
|
36
39
|
/**
|
|
37
40
|
* Register all real tool handlers on an McpServer instance.
|
|
@@ -46,6 +49,7 @@ export function registerAllTools(server, config, api, relay, sessionManager) {
|
|
|
46
49
|
registerApprovePermission(server, relay, sessionManager);
|
|
47
50
|
registerDenyPermission(server, relay, sessionManager);
|
|
48
51
|
registerInterruptSession(server, relay, sessionManager);
|
|
52
|
+
registerStopSession(server, relay, sessionManager);
|
|
49
53
|
registerAnswerQuestion(server, api, relay, sessionManager);
|
|
50
54
|
if (config.enableStart) {
|
|
51
55
|
registerStartSession(server, api, relay, sessionManager, config);
|
|
@@ -73,11 +77,11 @@ export function createUnauthenticatedServer(config, onToolCallWhileUnauthenticat
|
|
|
73
77
|
};
|
|
74
78
|
function registerStubs() {
|
|
75
79
|
stubRegistrations = [];
|
|
76
|
-
for (const [name, desc] of TOOL_STUBS) {
|
|
77
|
-
stubRegistrations.push(server.tool(name, desc,
|
|
80
|
+
for (const [name, desc, schema] of TOOL_STUBS) {
|
|
81
|
+
stubRegistrations.push(server.tool(name, desc, schema, stubHandler));
|
|
78
82
|
}
|
|
79
83
|
if (config.enableStart) {
|
|
80
|
-
stubRegistrations.push(server.tool('start_session', 'Start a new Happy Coder session.',
|
|
84
|
+
stubRegistrations.push(server.tool('start_session', 'Start a new Happy Coder session.', startSessionSchema, stubHandler));
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
// Start with stubs
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { randomUUID } from 'crypto';
|
|
3
2
|
import { encryptToBase64 } from '../auth/crypto.js';
|
|
3
|
+
import { answerQuestionSchema } from './schemas.js';
|
|
4
4
|
export function registerAnswerQuestion(server, api, relay, sessionManager) {
|
|
5
|
-
return server.tool('answer_question', 'Answer a question (AskUserQuestion) from a session. Approves the pending permission and sends a user message with the answer.', {
|
|
6
|
-
sessionId: z.string().describe('The session ID'),
|
|
7
|
-
requestId: z.string().describe('The question request ID from pending permissions'),
|
|
8
|
-
answers: z.record(z.string(), z.string()).describe('Map of question header to selected answer'),
|
|
9
|
-
}, async ({ sessionId, requestId, answers }) => {
|
|
5
|
+
return server.tool('answer_question', 'Answer a question (AskUserQuestion) from a session. Approves the pending permission and sends a user message with the answer.', answerQuestionSchema, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionId, requestId, answers }) => {
|
|
10
6
|
try {
|
|
11
7
|
// Check relay is connected (REQUIRED for answer_question)
|
|
12
8
|
if (!relay.connected) {
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { logger } from '../logger.js';
|
|
3
|
-
|
|
4
|
-
const DECISIONS = ['approved', 'approved_for_session'];
|
|
2
|
+
import { approvePermissionSchema } from './schemas.js';
|
|
5
3
|
export function registerApprovePermission(server, relay, sessionManager) {
|
|
6
|
-
return server.tool('approve_permission', 'Approve a pending permission request in a session. Permission requests are shown in get_session and watch_session results.', {
|
|
7
|
-
sessionId: z.string().describe('The session ID'),
|
|
8
|
-
requestId: z.string().describe('The permission request ID to approve, or "all_pending" to approve all'),
|
|
9
|
-
mode: z.enum(PERMISSION_MODES).optional().describe('Permission mode to set after approval'),
|
|
10
|
-
allowTools: z.array(z.string()).optional().describe('List of tools to allow'),
|
|
11
|
-
decision: z.enum(DECISIONS).optional().default('approved').describe('Approval decision type'),
|
|
12
|
-
}, async ({ sessionId, requestId, mode, allowTools, decision }) => {
|
|
4
|
+
return server.tool('approve_permission', 'Approve a pending permission request in a session. Permission requests are shown in get_session and watch_session results.', approvePermissionSchema, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionId, requestId, mode, allowTools, decision }) => {
|
|
13
5
|
try {
|
|
14
6
|
if (!relay.connected) {
|
|
15
7
|
const msg = relay.state === 'connecting'
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { logger } from '../logger.js';
|
|
2
|
+
import { denyPermissionSchema } from './schemas.js';
|
|
3
3
|
export function registerDenyPermission(server, relay, sessionManager) {
|
|
4
|
-
return server.tool('deny_permission', 'Deny a pending permission request in a session. Can also abort the entire session.', {
|
|
5
|
-
sessionId: z.string().describe('The session ID'),
|
|
6
|
-
requestId: z.string().describe('The permission request ID to deny'),
|
|
7
|
-
reason: z.string().optional().describe('Feedback text explaining why the request was denied'),
|
|
8
|
-
decision: z.enum(['denied', 'abort']).optional().default('denied').describe('Denial type: denied (reject this request) or abort (stop the session)'),
|
|
9
|
-
}, async ({ sessionId, requestId, reason, decision }) => {
|
|
4
|
+
return server.tool('deny_permission', 'Deny a pending permission request in a session. Can also abort the entire session.', denyPermissionSchema, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionId, requestId, reason, decision }) => {
|
|
10
5
|
try {
|
|
11
6
|
if (!relay.connected) {
|
|
12
7
|
const msg = relay.state === 'connecting'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { decodeBase64, decrypt } from '../auth/crypto.js';
|
|
2
|
+
import { getSessionSchema } from './schemas.js';
|
|
3
3
|
function renderMessage(msg) {
|
|
4
4
|
const envelope = msg.content;
|
|
5
5
|
if (!envelope)
|
|
@@ -47,12 +47,7 @@ function renderMessage(msg) {
|
|
|
47
47
|
return { id: msg.id, role, content: text, timestamp: new Date(msg.createdAt).toISOString() };
|
|
48
48
|
}
|
|
49
49
|
export function registerGetSession(server, api, sessionManager) {
|
|
50
|
-
return server.tool('get_session', 'Get detailed information about a specific session including recent messages, metadata, and pending permissions.', {
|
|
51
|
-
sessionId: z.string().describe('The session ID to get details for'),
|
|
52
|
-
includeMessages: z.boolean().optional().default(true).describe('Whether to include messages'),
|
|
53
|
-
lastN: z.number().optional().default(20).describe('Number of recent messages to include'),
|
|
54
|
-
after: z.string().optional().describe('ISO 8601 timestamp — only return messages after this time'),
|
|
55
|
-
}, async ({ sessionId, includeMessages, lastN, after }) => {
|
|
50
|
+
return server.tool('get_session', 'Get detailed information about a specific session including recent messages, metadata, and pending permissions.', getSessionSchema, { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionId, includeMessages, lastN, after }) => {
|
|
56
51
|
try {
|
|
57
52
|
const session = sessionManager.get(sessionId);
|
|
58
53
|
if (!session) {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { logger } from '../logger.js';
|
|
2
|
+
import { interruptSessionSchema } from './schemas.js';
|
|
3
3
|
export function registerInterruptSession(server, relay, sessionManager) {
|
|
4
|
-
return server.tool('interrupt_session', 'Interrupt a running Claude Code session to stop its current activity. This sends an abort signal equivalent to pressing Escape — the session stays alive and can accept new messages afterward. Use this when a session is actively generating output and you need it to stop.', {
|
|
5
|
-
sessionId: z.string().describe('The session ID to interrupt'),
|
|
6
|
-
}, async ({ sessionId }) => {
|
|
4
|
+
return server.tool('interrupt_session', 'Interrupt a running Claude Code session to stop its current activity. This sends an abort signal equivalent to pressing Escape — the session stays alive and can accept new messages afterward. Use this when a session is actively generating output and you need it to stop.', interruptSessionSchema, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionId }) => {
|
|
7
5
|
try {
|
|
8
6
|
if (!relay.connected) {
|
|
9
7
|
const msg = relay.state === 'connecting'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { listComputersSchema } from './schemas.js';
|
|
1
2
|
export function registerListComputers(server, sessionManager, config) {
|
|
2
|
-
return server.tool('list_computers', 'List available computers filtered by HAPPY_MCP_COMPUTERS. Shows hostname, online status, and active session count. Use before start_session to find available machines.', {}, async () => {
|
|
3
|
+
return server.tool('list_computers', 'List available computers filtered by HAPPY_MCP_COMPUTERS. Shows hostname, online status, and active session count. Use before start_session to find available machines.', listComputersSchema, { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async () => {
|
|
3
4
|
try {
|
|
4
5
|
const allMachines = sessionManager.getAllMachines();
|
|
5
6
|
const machines = allMachines
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listSessionsSchema } from './schemas.js';
|
|
2
2
|
export function registerListSessions(server, sessionManager, config) {
|
|
3
|
-
return server.tool('list_sessions', 'List all active Happy Coder sessions. Shows session ID, project path, hostname, status (active/idle/waiting_permission), and pending permissions.', {
|
|
4
|
-
filter: z.object({
|
|
5
|
-
status: z.enum(['active', 'idle', 'waiting_permission']).optional().describe('Filter by session status'),
|
|
6
|
-
computer: z.string().optional().describe('Filter by computer hostname'),
|
|
7
|
-
projectPath: z.string().optional().describe('Filter by project path prefix'),
|
|
8
|
-
}).optional(),
|
|
9
|
-
}, async ({ filter }) => {
|
|
3
|
+
return server.tool('list_sessions', 'List all active Happy Coder sessions. Shows session ID, project path, hostname, status (active/idle/waiting_permission), and pending permissions.', listSessionsSchema, { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ filter }) => {
|
|
10
4
|
try {
|
|
11
5
|
const computerFilter = filter?.computer ? [filter.computer] : config.computers;
|
|
12
6
|
const pathFilter = filter?.projectPath ? [filter.projectPath] : config.projectPaths;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Centralized Zod schemas for all MCP tools.
|
|
4
|
+
* These schemas are the single source of truth for tool input validation.
|
|
5
|
+
* They are used both for real tool registration and for stub registration
|
|
6
|
+
* to ensure clients always see correct parameter schemas.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PERMISSION_MODES: readonly ["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"];
|
|
9
|
+
export declare const APPROVAL_DECISIONS: readonly ["approved", "approved_for_session"];
|
|
10
|
+
export declare const DENIAL_DECISIONS: readonly ["denied", "abort"];
|
|
11
|
+
export declare const AGENTS: readonly ["claude", "codex", "gemini"];
|
|
12
|
+
export declare const listComputersSchema: {};
|
|
13
|
+
export declare const listSessionsSchema: {
|
|
14
|
+
filter: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
status: z.ZodOptional<z.ZodEnum<["active", "idle", "waiting_permission"]>>;
|
|
16
|
+
computer: z.ZodOptional<z.ZodString>;
|
|
17
|
+
projectPath: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
status?: "active" | "idle" | "waiting_permission" | undefined;
|
|
20
|
+
computer?: string | undefined;
|
|
21
|
+
projectPath?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
status?: "active" | "idle" | "waiting_permission" | undefined;
|
|
24
|
+
computer?: string | undefined;
|
|
25
|
+
projectPath?: string | undefined;
|
|
26
|
+
}>>;
|
|
27
|
+
};
|
|
28
|
+
export declare const getSessionSchema: {
|
|
29
|
+
sessionId: z.ZodString;
|
|
30
|
+
includeMessages: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
31
|
+
lastN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
32
|
+
after: z.ZodOptional<z.ZodString>;
|
|
33
|
+
};
|
|
34
|
+
export declare const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
35
|
+
export declare const watchSessionSchema: {
|
|
36
|
+
sessionIds: z.ZodArray<z.ZodString, "many">;
|
|
37
|
+
timeoutSeconds: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
38
|
+
};
|
|
39
|
+
export declare const sendMessageSchema: {
|
|
40
|
+
sessionId: z.ZodString;
|
|
41
|
+
message: z.ZodString;
|
|
42
|
+
meta: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
permissionMode: z.ZodOptional<z.ZodEnum<["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"]>>;
|
|
44
|
+
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
45
|
+
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "read-only" | "safe-yolo" | "yolo" | undefined;
|
|
48
|
+
allowedTools?: string[] | undefined;
|
|
49
|
+
disallowedTools?: string[] | undefined;
|
|
50
|
+
}, {
|
|
51
|
+
permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "read-only" | "safe-yolo" | "yolo" | undefined;
|
|
52
|
+
allowedTools?: string[] | undefined;
|
|
53
|
+
disallowedTools?: string[] | undefined;
|
|
54
|
+
}>>;
|
|
55
|
+
};
|
|
56
|
+
export declare const approvePermissionSchema: {
|
|
57
|
+
sessionId: z.ZodString;
|
|
58
|
+
requestId: z.ZodString;
|
|
59
|
+
mode: z.ZodOptional<z.ZodEnum<["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"]>>;
|
|
60
|
+
allowTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
61
|
+
decision: z.ZodDefault<z.ZodOptional<z.ZodEnum<["approved", "approved_for_session"]>>>;
|
|
62
|
+
};
|
|
63
|
+
export declare const denyPermissionSchema: {
|
|
64
|
+
sessionId: z.ZodString;
|
|
65
|
+
requestId: z.ZodString;
|
|
66
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
67
|
+
decision: z.ZodDefault<z.ZodOptional<z.ZodEnum<["denied", "abort"]>>>;
|
|
68
|
+
};
|
|
69
|
+
export declare const interruptSessionSchema: {
|
|
70
|
+
sessionId: z.ZodString;
|
|
71
|
+
};
|
|
72
|
+
export declare const answerQuestionSchema: {
|
|
73
|
+
sessionId: z.ZodString;
|
|
74
|
+
requestId: z.ZodString;
|
|
75
|
+
answers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
76
|
+
};
|
|
77
|
+
export declare const stopSessionSchema: {
|
|
78
|
+
sessionId: z.ZodString;
|
|
79
|
+
};
|
|
80
|
+
export declare const startSessionSchema: {
|
|
81
|
+
computer: z.ZodString;
|
|
82
|
+
projectPath: z.ZodString;
|
|
83
|
+
initialMessage: z.ZodOptional<z.ZodString>;
|
|
84
|
+
permissionMode: z.ZodOptional<z.ZodEnum<["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"]>>;
|
|
85
|
+
agent: z.ZodDefault<z.ZodOptional<z.ZodEnum<["claude", "codex", "gemini"]>>>;
|
|
86
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Centralized Zod schemas for all MCP tools.
|
|
4
|
+
* These schemas are the single source of truth for tool input validation.
|
|
5
|
+
* They are used both for real tool registration and for stub registration
|
|
6
|
+
* to ensure clients always see correct parameter schemas.
|
|
7
|
+
*/
|
|
8
|
+
// Permission modes (used across multiple tools)
|
|
9
|
+
export const PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo'];
|
|
10
|
+
// Decision types for approve_permission
|
|
11
|
+
export const APPROVAL_DECISIONS = ['approved', 'approved_for_session'];
|
|
12
|
+
// Decision types for deny_permission
|
|
13
|
+
export const DENIAL_DECISIONS = ['denied', 'abort'];
|
|
14
|
+
// AI agents for start_session
|
|
15
|
+
export const AGENTS = ['claude', 'codex', 'gemini'];
|
|
16
|
+
// list_computers (no parameters)
|
|
17
|
+
export const listComputersSchema = {};
|
|
18
|
+
// list_sessions
|
|
19
|
+
export const listSessionsSchema = {
|
|
20
|
+
filter: z.object({
|
|
21
|
+
status: z.enum(['active', 'idle', 'waiting_permission']).optional().describe('Filter by session status'),
|
|
22
|
+
computer: z.string().optional().describe('Filter by computer hostname'),
|
|
23
|
+
projectPath: z.string().optional().describe('Filter by project path prefix'),
|
|
24
|
+
}).optional(),
|
|
25
|
+
};
|
|
26
|
+
// get_session
|
|
27
|
+
export const getSessionSchema = {
|
|
28
|
+
sessionId: z.string().describe('The session ID to get details for'),
|
|
29
|
+
includeMessages: z.boolean().optional().default(true).describe('Whether to include messages'),
|
|
30
|
+
lastN: z.number().optional().default(20).describe('Number of recent messages to include'),
|
|
31
|
+
after: z.string().optional().describe('ISO 8601 timestamp — only return messages after this time'),
|
|
32
|
+
};
|
|
33
|
+
// watch_session
|
|
34
|
+
export const DEFAULT_TIMEOUT_SECONDS = 300; // 5 minutes
|
|
35
|
+
export const watchSessionSchema = {
|
|
36
|
+
sessionIds: z.array(z.string()).describe('One or more session IDs to watch'),
|
|
37
|
+
timeoutSeconds: z.number().optional().default(DEFAULT_TIMEOUT_SECONDS).describe('Max wait time in seconds (default 300)'),
|
|
38
|
+
};
|
|
39
|
+
// send_message
|
|
40
|
+
export const sendMessageSchema = {
|
|
41
|
+
sessionId: z.string().describe('The session ID to send the message to'),
|
|
42
|
+
message: z.string().describe('The message text to send'),
|
|
43
|
+
meta: z.object({
|
|
44
|
+
permissionMode: z.enum(PERMISSION_MODES).optional(),
|
|
45
|
+
allowedTools: z.array(z.string()).optional(),
|
|
46
|
+
disallowedTools: z.array(z.string()).optional(),
|
|
47
|
+
}).optional(),
|
|
48
|
+
};
|
|
49
|
+
// approve_permission
|
|
50
|
+
export const approvePermissionSchema = {
|
|
51
|
+
sessionId: z.string().describe('The session ID'),
|
|
52
|
+
requestId: z.string().describe('The permission request ID to approve, or "all_pending" to approve all'),
|
|
53
|
+
mode: z.enum(PERMISSION_MODES).optional().describe('Permission mode to set after approval'),
|
|
54
|
+
allowTools: z.array(z.string()).optional().describe('List of tools to allow'),
|
|
55
|
+
decision: z.enum(APPROVAL_DECISIONS).optional().default('approved').describe('Approval decision type'),
|
|
56
|
+
};
|
|
57
|
+
// deny_permission
|
|
58
|
+
export const denyPermissionSchema = {
|
|
59
|
+
sessionId: z.string().describe('The session ID'),
|
|
60
|
+
requestId: z.string().describe('The permission request ID to deny'),
|
|
61
|
+
reason: z.string().optional().describe('Feedback text explaining why the request was denied'),
|
|
62
|
+
decision: z.enum(DENIAL_DECISIONS).optional().default('denied').describe('Denial type: denied (reject this request) or abort (stop the session)'),
|
|
63
|
+
};
|
|
64
|
+
// interrupt_session
|
|
65
|
+
export const interruptSessionSchema = {
|
|
66
|
+
sessionId: z.string().describe('The session ID to interrupt'),
|
|
67
|
+
};
|
|
68
|
+
// answer_question
|
|
69
|
+
export const answerQuestionSchema = {
|
|
70
|
+
sessionId: z.string().describe('The session ID'),
|
|
71
|
+
requestId: z.string().describe('The question request ID from pending permissions'),
|
|
72
|
+
answers: z.record(z.string(), z.string()).describe('Map of question header to selected answer'),
|
|
73
|
+
};
|
|
74
|
+
// stop_session
|
|
75
|
+
export const stopSessionSchema = {
|
|
76
|
+
sessionId: z.string().describe('The session ID to stop'),
|
|
77
|
+
};
|
|
78
|
+
// start_session (uses a generic description for projectPath)
|
|
79
|
+
export const startSessionSchema = {
|
|
80
|
+
computer: z.string().describe('The machine ID (from list_computers)'),
|
|
81
|
+
projectPath: z.string().describe('The working directory for the new session'),
|
|
82
|
+
initialMessage: z.string().optional().describe('Initial message to send after session starts'),
|
|
83
|
+
permissionMode: z.enum(PERMISSION_MODES).optional().describe('Permission mode for the session'),
|
|
84
|
+
agent: z.enum(AGENTS).optional().default('claude').describe('The AI agent to use'),
|
|
85
|
+
};
|
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
1
|
import { encryptToBase64 } from '../auth/crypto.js';
|
|
3
|
-
|
|
2
|
+
import { sendMessageSchema } from './schemas.js';
|
|
4
3
|
export function registerSendMessage(server, relay, sessionManager) {
|
|
5
|
-
return server.tool('send_message', 'Send a message to a Happy Coder session. The message will appear as a user message in the session. If the session is actively generating, the message will be queued and processed when the session is ready. Can optionally change the permission mode.', {
|
|
6
|
-
sessionId: z.string().describe('The session ID to send the message to'),
|
|
7
|
-
message: z.string().describe('The message text to send'),
|
|
8
|
-
meta: z.object({
|
|
9
|
-
permissionMode: z.enum(PERMISSION_MODES).optional(),
|
|
10
|
-
allowedTools: z.array(z.string()).optional(),
|
|
11
|
-
disallowedTools: z.array(z.string()).optional(),
|
|
12
|
-
}).optional(),
|
|
13
|
-
}, async ({ sessionId, message, meta }) => {
|
|
4
|
+
return server.tool('send_message', 'Send a message to a Happy Coder session. The message will appear as a user message in the session. If the session is actively generating, the message will be queued and processed when the session is ready. Can optionally change the permission mode.', sendMessageSchema, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionId, message, meta }) => {
|
|
14
5
|
try {
|
|
15
6
|
// Check relay connectivity
|
|
16
7
|
if (!relay.connected) {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
import { startSessionSchema } from './schemas.js';
|
|
3
3
|
export function registerStartSession(server, _api, relay, sessionManager, config) {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// Create a dynamic schema with config-specific projectPath description
|
|
5
|
+
const dynamicSchema = {
|
|
6
|
+
...startSessionSchema,
|
|
6
7
|
projectPath: z.string().describe(config.projectPaths.includes('*')
|
|
7
8
|
? 'The working directory for the new session'
|
|
8
9
|
: `The working directory for the new session. Allowed paths: ${config.projectPaths.join(', ')}`),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
agent: z.enum(['claude', 'codex', 'gemini']).optional().default('claude').describe('The AI agent to use'),
|
|
12
|
-
}, async ({ computer, projectPath, initialMessage, permissionMode, agent }) => {
|
|
10
|
+
};
|
|
11
|
+
return server.tool('start_session', 'Start a new Happy Coder session on a remote machine. Requires the machine to be online. Use list_computers to find available machines.', dynamicSchema, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ computer, projectPath, initialMessage, permissionMode, agent }) => {
|
|
13
12
|
try {
|
|
14
13
|
if (!relay.connected) {
|
|
15
14
|
const msg = relay.state === 'connecting'
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { RelayClient } from '../relay/client.js';
|
|
3
|
+
import type { SessionManager } from '../session/manager.js';
|
|
4
|
+
export declare function registerStopSession(server: McpServer, relay: RelayClient, sessionManager: SessionManager): RegisteredTool;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
import { stopSessionSchema } from './schemas.js';
|
|
3
|
+
export function registerStopSession(server, relay, sessionManager) {
|
|
4
|
+
return server.tool('stop_session', 'Stop and archive a running Happy Coder session. This gracefully terminates the CLI process — the session becomes inactive but its message history is preserved. Use this when you\'re done with a session and want to free up resources.', stopSessionSchema, { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, async ({ sessionId }) => {
|
|
5
|
+
try {
|
|
6
|
+
if (!relay.connected) {
|
|
7
|
+
const msg = relay.state === 'connecting'
|
|
8
|
+
? 'Relay is still connecting. Please try again in a few seconds.'
|
|
9
|
+
: 'Relay is disconnected.';
|
|
10
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({
|
|
11
|
+
error: relay.state === 'connecting' ? 'RelayConnecting' : 'RelayDisconnected',
|
|
12
|
+
message: msg,
|
|
13
|
+
}) }] };
|
|
14
|
+
}
|
|
15
|
+
const session = sessionManager.get(sessionId);
|
|
16
|
+
if (!session) {
|
|
17
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({ error: 'SessionNotFound', message: `Session ${sessionId} not found` }) }] };
|
|
18
|
+
}
|
|
19
|
+
await relay.sessionRpc(sessionId, 'killSession', {});
|
|
20
|
+
logger.info(`[audit] Session STOPPED: session=${sessionId}`);
|
|
21
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, sessionId, message: 'Session stopped successfully. The CLI process has been terminated.' }, null, 2) }] };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({ error: 'StopSessionFailed', message: err.message }) }] };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const DEFAULT_TIMEOUT_SECONDS = 300; // 5 minutes
|
|
1
|
+
import { watchSessionSchema } from './schemas.js';
|
|
3
2
|
const MAX_WAIT_MS = 300_000; // 5 min max wait
|
|
4
3
|
export function registerWatchSession(server, relay, sessionManager) {
|
|
5
|
-
return server.tool('watch_session', 'Watch one or more sessions for state changes. Returns immediately if any session is idle or has pending permissions. Otherwise waits up to 5 minutes for a state change. If sessions are still active, returns current status -- call again to continue watching.', {
|
|
6
|
-
sessionIds: z.array(z.string()).describe('One or more session IDs to watch'),
|
|
7
|
-
timeoutSeconds: z.number().optional().default(DEFAULT_TIMEOUT_SECONDS).describe('Max wait time in seconds (default 300)'),
|
|
8
|
-
}, async ({ sessionIds, timeoutSeconds }, extra) => {
|
|
4
|
+
return server.tool('watch_session', 'Watch one or more sessions for state changes. Returns immediately if any session is idle or has pending permissions. Otherwise waits up to 5 minutes for a state change. If sessions are still active, returns current status -- call again to continue watching.', watchSessionSchema, { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ sessionIds, timeoutSeconds }, extra) => {
|
|
9
5
|
try {
|
|
10
6
|
// Validate all session IDs exist
|
|
11
7
|
const missing = sessionIds.filter(id => !sessionManager.get(id));
|