happy-mcp-server 1.1.2 → 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 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,7 +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 { listComputersSchema, listSessionsSchema, getSessionSchema, watchSessionSchema, sendMessageSchema, approvePermissionSchema, denyPermissionSchema, interruptSessionSchema, answerQuestionSchema, startSessionSchema, } from './tools/schemas.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';
13
14
  const AUTH_ERROR = {
14
15
  isError: true,
15
16
  content: [{
@@ -32,6 +33,7 @@ const TOOL_STUBS = [
32
33
  ['approve_permission', 'Approve a pending permission request.', approvePermissionSchema],
33
34
  ['deny_permission', 'Deny a pending permission request.', denyPermissionSchema],
34
35
  ['interrupt_session', 'Interrupt a running session to stop its current activity.', interruptSessionSchema],
36
+ ['stop_session', 'Stop and archive a running session.', stopSessionSchema],
35
37
  ['answer_question', 'Answer a question from a session.', answerQuestionSchema],
36
38
  ];
37
39
  /**
@@ -47,6 +49,7 @@ export function registerAllTools(server, config, api, relay, sessionManager) {
47
49
  registerApprovePermission(server, relay, sessionManager);
48
50
  registerDenyPermission(server, relay, sessionManager);
49
51
  registerInterruptSession(server, relay, sessionManager);
52
+ registerStopSession(server, relay, sessionManager);
50
53
  registerAnswerQuestion(server, api, relay, sessionManager);
51
54
  if (config.enableStart) {
52
55
  registerStartSession(server, api, relay, sessionManager, config);
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
2
2
  import { encryptToBase64 } from '../auth/crypto.js';
3
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.', answerQuestionSchema, 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 }) => {
6
6
  try {
7
7
  // Check relay is connected (REQUIRED for answer_question)
8
8
  if (!relay.connected) {
@@ -1,7 +1,7 @@
1
1
  import { logger } from '../logger.js';
2
2
  import { approvePermissionSchema } from './schemas.js';
3
3
  export function registerApprovePermission(server, relay, sessionManager) {
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, 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 }) => {
5
5
  try {
6
6
  if (!relay.connected) {
7
7
  const msg = relay.state === 'connecting'
@@ -1,7 +1,7 @@
1
1
  import { logger } from '../logger.js';
2
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.', denyPermissionSchema, 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 }) => {
5
5
  try {
6
6
  if (!relay.connected) {
7
7
  const msg = relay.state === 'connecting'
@@ -47,7 +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.', getSessionSchema, 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 }) => {
51
51
  try {
52
52
  const session = sessionManager.get(sessionId);
53
53
  if (!session) {
@@ -1,7 +1,7 @@
1
1
  import { logger } from '../logger.js';
2
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.', interruptSessionSchema, 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 }) => {
5
5
  try {
6
6
  if (!relay.connected) {
7
7
  const msg = relay.state === 'connecting'
@@ -1,6 +1,6 @@
1
1
  import { listComputersSchema } from './schemas.js';
2
2
  export function registerListComputers(server, sessionManager, config) {
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, 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 () => {
4
4
  try {
5
5
  const allMachines = sessionManager.getAllMachines();
6
6
  const machines = allMachines
@@ -1,6 +1,6 @@
1
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.', listSessionsSchema, 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 }) => {
4
4
  try {
5
5
  const computerFilter = filter?.computer ? [filter.computer] : config.computers;
6
6
  const pathFilter = filter?.projectPath ? [filter.projectPath] : config.projectPaths;
@@ -74,6 +74,9 @@ export declare const answerQuestionSchema: {
74
74
  requestId: z.ZodString;
75
75
  answers: z.ZodRecord<z.ZodString, z.ZodString>;
76
76
  };
77
+ export declare const stopSessionSchema: {
78
+ sessionId: z.ZodString;
79
+ };
77
80
  export declare const startSessionSchema: {
78
81
  computer: z.ZodString;
79
82
  projectPath: z.ZodString;
@@ -71,6 +71,10 @@ export const answerQuestionSchema = {
71
71
  requestId: z.string().describe('The question request ID from pending permissions'),
72
72
  answers: z.record(z.string(), z.string()).describe('Map of question header to selected answer'),
73
73
  };
74
+ // stop_session
75
+ export const stopSessionSchema = {
76
+ sessionId: z.string().describe('The session ID to stop'),
77
+ };
74
78
  // start_session (uses a generic description for projectPath)
75
79
  export const startSessionSchema = {
76
80
  computer: z.string().describe('The machine ID (from list_computers)'),
@@ -1,7 +1,7 @@
1
1
  import { encryptToBase64 } from '../auth/crypto.js';
2
2
  import { sendMessageSchema } from './schemas.js';
3
3
  export function registerSendMessage(server, relay, sessionManager) {
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, 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 }) => {
5
5
  try {
6
6
  // Check relay connectivity
7
7
  if (!relay.connected) {
@@ -8,7 +8,7 @@ export function registerStartSession(server, _api, relay, sessionManager, config
8
8
  ? 'The working directory for the new session'
9
9
  : `The working directory for the new session. Allowed paths: ${config.projectPaths.join(', ')}`),
10
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, async ({ computer, projectPath, initialMessage, permissionMode, agent }) => {
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 }) => {
12
12
  try {
13
13
  if (!relay.connected) {
14
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,7 +1,7 @@
1
1
  import { watchSessionSchema } from './schemas.js';
2
2
  const MAX_WAIT_MS = 300_000; // 5 min max wait
3
3
  export function registerWatchSession(server, relay, sessionManager) {
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, 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) => {
5
5
  try {
6
6
  // Validate all session IDs exist
7
7
  const missing = sessionIds.filter(id => !sessionManager.get(id));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-mcp-server",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for observing and controlling Happy Coder sessions",
5
5
  "author": {
6
6
  "name": "Jared Spencer",