happy-mcp-server 0.2.0 → 0.3.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/dist/http.d.ts +1 -1
- package/dist/http.js +6 -5
- package/dist/index.js +56 -12
- package/dist/server.js +3 -0
- package/dist/tools/interrupt_session.d.ts +4 -0
- package/dist/tools/interrupt_session.js +31 -0
- package/package.json +1 -1
package/dist/http.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface HttpServerHandle {
|
|
|
11
11
|
* Creates a new McpServer instance per client session.
|
|
12
12
|
* All sessions share the same relay/sessionManager/api singletons.
|
|
13
13
|
*/
|
|
14
|
-
export declare function startHttpServer(config: Config, api: ApiClient, relay: RelayClient, sessionManager: SessionManager): Promise<HttpServerHandle>;
|
|
14
|
+
export declare function startHttpServer(config: Config, api: ApiClient, relay: RelayClient, sessionManager: SessionManager, port?: number): Promise<HttpServerHandle>;
|
package/dist/http.js
CHANGED
|
@@ -10,7 +10,7 @@ import { logger } from './logger.js';
|
|
|
10
10
|
* Creates a new McpServer instance per client session.
|
|
11
11
|
* All sessions share the same relay/sessionManager/api singletons.
|
|
12
12
|
*/
|
|
13
|
-
export async function startHttpServer(config, api, relay, sessionManager) {
|
|
13
|
+
export async function startHttpServer(config, api, relay, sessionManager, port = 0) {
|
|
14
14
|
const app = createMcpExpressApp();
|
|
15
15
|
const transports = new Map();
|
|
16
16
|
// POST /mcp - handle initialization and subsequent requests
|
|
@@ -134,7 +134,7 @@ export async function startHttpServer(config, api, relay, sessionManager) {
|
|
|
134
134
|
});
|
|
135
135
|
// Start listening on auto-assigned port
|
|
136
136
|
const httpServer = await new Promise((resolve, reject) => {
|
|
137
|
-
const server = app.listen(
|
|
137
|
+
const server = app.listen(port, '127.0.0.1', (error) => {
|
|
138
138
|
if (error) {
|
|
139
139
|
reject(error);
|
|
140
140
|
}
|
|
@@ -142,13 +142,14 @@ export async function startHttpServer(config, api, relay, sessionManager) {
|
|
|
142
142
|
resolve(server);
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
|
+
server.on('error', reject);
|
|
145
146
|
});
|
|
146
147
|
const addr = httpServer.address();
|
|
147
148
|
if (!addr || typeof addr === 'string') {
|
|
148
149
|
throw new Error('Failed to get server address');
|
|
149
150
|
}
|
|
150
|
-
const
|
|
151
|
-
logger.debug(`HTTP server listening on http://127.0.0.1:${
|
|
151
|
+
const actualPort = addr.port;
|
|
152
|
+
logger.debug(`HTTP server listening on http://127.0.0.1:${actualPort}/mcp`);
|
|
152
153
|
// Cleanup function
|
|
153
154
|
const close = async () => {
|
|
154
155
|
logger.debug('Closing HTTP server...');
|
|
@@ -175,5 +176,5 @@ export async function startHttpServer(config, api, relay, sessionManager) {
|
|
|
175
176
|
});
|
|
176
177
|
logger.debug('HTTP server closed');
|
|
177
178
|
};
|
|
178
|
-
return { port, close };
|
|
179
|
+
return { port: actualPort, close };
|
|
179
180
|
}
|
package/dist/index.js
CHANGED
|
@@ -152,17 +152,44 @@ async function initializeAuthenticatedState(config, credentials) {
|
|
|
152
152
|
// CLI Commands
|
|
153
153
|
// ---------------------------------------------------------------------------
|
|
154
154
|
function printHelp() {
|
|
155
|
-
console.error(`Usage: happy-mcp [command]
|
|
155
|
+
console.error(`Usage: happy-mcp [command] [options]
|
|
156
156
|
|
|
157
157
|
Commands:
|
|
158
|
-
(no args)
|
|
159
|
-
serve
|
|
160
|
-
auth
|
|
161
|
-
auth login
|
|
162
|
-
auth logout
|
|
163
|
-
help
|
|
158
|
+
(no args) Start the MCP server (stdio transport)
|
|
159
|
+
serve [--port <port>] Start the MCP server (HTTP transport)
|
|
160
|
+
auth Check auth status, or pair if not authenticated
|
|
161
|
+
auth login Run the QR pairing flow (re-auth if already paired)
|
|
162
|
+
auth logout Remove saved credentials
|
|
163
|
+
help Show this help message
|
|
164
|
+
|
|
165
|
+
Options for serve:
|
|
166
|
+
--port <port> Bind to a specific port (default: auto-assign)
|
|
164
167
|
`);
|
|
165
168
|
}
|
|
169
|
+
function parseServeFlags(argv) {
|
|
170
|
+
let port;
|
|
171
|
+
for (let i = 0; i < argv.length; i++) {
|
|
172
|
+
if (argv[i] === '--port') {
|
|
173
|
+
const raw = argv[i + 1];
|
|
174
|
+
if (raw === undefined || raw.startsWith('-')) {
|
|
175
|
+
console.error('[happy-mcp] --port requires a port number. Example: happy-mcp serve --port 3000');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
const parsed = Number(raw);
|
|
179
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
180
|
+
console.error(`[happy-mcp] Invalid port '${raw}'. Must be an integer between 1 and 65535.`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
port = parsed;
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.error(`[happy-mcp] Unknown flag '${argv[i]}'. Run \`happy-mcp help\` for help.`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { port };
|
|
192
|
+
}
|
|
166
193
|
async function runAuth() {
|
|
167
194
|
const config = loadConfig();
|
|
168
195
|
const creds = readCredentials(config.credentialsPath);
|
|
@@ -288,7 +315,7 @@ async function runServer() {
|
|
|
288
315
|
// ---------------------------------------------------------------------------
|
|
289
316
|
// HTTP Server
|
|
290
317
|
// ---------------------------------------------------------------------------
|
|
291
|
-
async function runHttpServer() {
|
|
318
|
+
async function runHttpServer(port) {
|
|
292
319
|
const config = loadConfig();
|
|
293
320
|
setupLogger(config.logLevel);
|
|
294
321
|
const creds = readCredentials(config.credentialsPath);
|
|
@@ -305,11 +332,27 @@ async function runHttpServer() {
|
|
|
305
332
|
}
|
|
306
333
|
logger.info('Starting happy-mcp HTTP server...');
|
|
307
334
|
const state = await initializeAuthenticatedState(config, creds);
|
|
308
|
-
|
|
309
|
-
|
|
335
|
+
let result;
|
|
336
|
+
try {
|
|
337
|
+
result = await startHttpServer(config, state.api, state.relay, state.sessionManager, port ?? 0);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
const err = error;
|
|
341
|
+
if (err.code === 'EADDRINUSE') {
|
|
342
|
+
console.error(`[happy-mcp] Port ${port} is already in use.`);
|
|
343
|
+
}
|
|
344
|
+
else if (err.code === 'EACCES') {
|
|
345
|
+
console.error(`[happy-mcp] Permission denied binding to port ${port}. Try a port above 1023.`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
console.error(`[happy-mcp] Failed to start HTTP server: ${err.message ?? err}`);
|
|
349
|
+
}
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
console.error(`[happy-mcp] HTTP server listening on http://127.0.0.1:${result.port}/mcp`);
|
|
310
353
|
const shutdown = async () => {
|
|
311
354
|
logger.info('Shutting down HTTP server...');
|
|
312
|
-
await close();
|
|
355
|
+
await result.close();
|
|
313
356
|
state.relay.disconnect();
|
|
314
357
|
state.sessionManager.destroy();
|
|
315
358
|
clearKeyCache();
|
|
@@ -329,7 +372,8 @@ if (!command) {
|
|
|
329
372
|
});
|
|
330
373
|
}
|
|
331
374
|
else if (command === 'serve') {
|
|
332
|
-
|
|
375
|
+
const flags = parseServeFlags(process.argv.slice(3));
|
|
376
|
+
runHttpServer(flags.port).catch((err) => {
|
|
333
377
|
console.error('[happy-mcp] Fatal:', err.message ?? err);
|
|
334
378
|
process.exit(1);
|
|
335
379
|
});
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerApprovePermission } from './tools/approve_permission.js';
|
|
|
8
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
|
+
import { registerInterruptSession } from './tools/interrupt_session.js';
|
|
11
12
|
const AUTH_ERROR = {
|
|
12
13
|
isError: true,
|
|
13
14
|
content: [{
|
|
@@ -29,6 +30,7 @@ const TOOL_STUBS = [
|
|
|
29
30
|
['send_message', 'Send a message to a session.'],
|
|
30
31
|
['approve_permission', 'Approve a pending permission request.'],
|
|
31
32
|
['deny_permission', 'Deny a pending permission request.'],
|
|
33
|
+
['interrupt_session', 'Interrupt a running session to stop its current activity.'],
|
|
32
34
|
['answer_question', 'Answer a question from a session.'],
|
|
33
35
|
];
|
|
34
36
|
/**
|
|
@@ -43,6 +45,7 @@ export function registerAllTools(server, config, api, relay, sessionManager) {
|
|
|
43
45
|
registerSendMessage(server, api, sessionManager);
|
|
44
46
|
registerApprovePermission(server, relay, sessionManager);
|
|
45
47
|
registerDenyPermission(server, relay, sessionManager);
|
|
48
|
+
registerInterruptSession(server, relay, sessionManager);
|
|
46
49
|
registerAnswerQuestion(server, api, relay, sessionManager);
|
|
47
50
|
if (config.enableStart) {
|
|
48
51
|
registerStartSession(server, api, relay, sessionManager, config);
|
|
@@ -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 registerInterruptSession(server: McpServer, relay: RelayClient, sessionManager: SessionManager): RegisteredTool;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
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 }) => {
|
|
7
|
+
try {
|
|
8
|
+
if (!relay.connected) {
|
|
9
|
+
const msg = relay.state === 'connecting'
|
|
10
|
+
? 'Relay is still connecting. Please try again in a few seconds.'
|
|
11
|
+
: 'Relay is disconnected.';
|
|
12
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({
|
|
13
|
+
error: relay.state === 'connecting' ? 'RelayConnecting' : 'RelayDisconnected',
|
|
14
|
+
message: msg,
|
|
15
|
+
}) }] };
|
|
16
|
+
}
|
|
17
|
+
const session = sessionManager.get(sessionId);
|
|
18
|
+
if (!session) {
|
|
19
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({ error: 'SessionNotFound', message: `Session ${sessionId} not found` }) }] };
|
|
20
|
+
}
|
|
21
|
+
await relay.sessionRpc(sessionId, 'abort', {
|
|
22
|
+
reason: "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.",
|
|
23
|
+
});
|
|
24
|
+
logger.info(`[audit] Session INTERRUPTED: session=${sessionId}`);
|
|
25
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, sessionId, message: 'Interrupt signal sent. The session will stop its current activity.' }, null, 2) }] };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({ error: 'InterruptFailed', message: err.message }) }] };
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|