happy-mcp-server 1.1.0 → 1.1.1

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
@@ -12,50 +12,66 @@ npm i -g happy-mcp-server
12
12
 
13
13
  ### 2. Authenticate
14
14
 
15
+ **Option A: MCP-based authentication (Recommended for Claude Cowork)**
16
+
17
+ Add the server to your MCP client configuration (see [MCP Configuration](#mcp-configuration) below), then call the `authentication_status` tool from within your MCP client. The tool returns a QR code and web URL for pairing. Once you scan the QR code with the Happy mobile app, tools activate automatically.
18
+
19
+ **Option B: CLI authentication**
20
+
21
+ ```bash
22
+ happy-mcp-server auth
23
+ ```
24
+
25
+ Scan the QR code with the Happy mobile app to pair your account. This creates credentials at `~/.happy-mcp/credentials.json`.
26
+
27
+ **Option C: Environment variable authentication**
28
+
29
+ For ephemeral environments (Claude Cowork, Docker, CI/CD), set the `HAPPY_MCP_ACCOUNT_SECRET` environment variable:
30
+
15
31
  ```bash
16
- happy-mcp auth
32
+ export HAPPY_MCP_ACCOUNT_SECRET=$(cat ~/.happy-mcp/credentials.json | jq -r .secret)
17
33
  ```
18
34
 
19
- Scan the QR code with the Happy mobile app to pair your account.
35
+ When set, authentication happens instantly on startup with no QR code or credential file required.
20
36
 
21
37
  ### 3. Choose Your Transport
22
38
 
23
39
  **Option A: Stdio Transport (Default)**
24
40
 
25
- Configure `happy-mcp` in your MCP client. See [MCP Configuration](#mcp-configuration) below.
41
+ Configure `happy-mcp-server` in your MCP client. See [MCP Configuration](#mcp-configuration) below.
26
42
 
27
43
  **Option B: HTTP Transport**
28
44
 
29
45
  Start the server with HTTP transport:
30
46
 
31
47
  ```bash
32
- happy-mcp serve
48
+ happy-mcp-server serve
33
49
  # Or specify a port:
34
- happy-mcp serve --port 3000
50
+ happy-mcp-server serve --port 3000
35
51
  ```
36
52
 
37
53
  The server will start at `http://127.0.0.1:<port>/mcp` (port auto-assigned if not specified).
38
54
 
39
55
  ## Commands
40
56
 
41
- `happy-mcp` provides several commands for different modes and authentication:
57
+ `happy-mcp-server` provides several commands for different modes and authentication:
42
58
 
43
59
  | Command | Description |
44
60
  |---------|-------------|
45
- | `happy-mcp` | Start the MCP server using stdio transport (default). Use this mode when configuring the server in MCP clients like Claude Desktop, Claude Code, Cursor, etc. |
46
- | `happy-mcp serve` | Start the MCP server using HTTP transport on an auto-assigned port. The server binds to `127.0.0.1` and reports the listening port and endpoint URL on startup. |
47
- | `happy-mcp serve --port <port>` | Start the HTTP server on a specific port (1-65535). Useful when you need a predictable port number for client configuration or firewall rules. |
48
- | `happy-mcp auth` | Check authentication status. If not authenticated, prompts you to scan a QR code with the Happy mobile app to pair your account. |
49
- | `happy-mcp auth login` | Force a new pairing flow, even if already authenticated. Use this to switch accounts or re-authenticate. |
50
- | `happy-mcp auth logout` | Remove saved credentials from `~/.happy-mcp/credentials.json`. |
51
- | `happy-mcp help` | Display help message with available commands. |
61
+ | `happy-mcp-server` | Start the MCP server using stdio transport (default). Use this mode when configuring the server in MCP clients like Claude Desktop, Claude Code, Cursor, etc. |
62
+ | `happy-mcp-server serve` | Start the MCP server using HTTP transport on an auto-assigned port. The server binds to `127.0.0.1` and reports the listening port and endpoint URL on startup. |
63
+ | `happy-mcp-server serve --port <port>` | Start the HTTP server on a specific port (1-65535). Useful when you need a predictable port number for client configuration or firewall rules. |
64
+ | `happy-mcp-server auth` | Check authentication status. If not authenticated, prompts you to scan a QR code with the Happy mobile app to pair your account. |
65
+ | `happy-mcp-server auth login` | Force a new pairing flow, even if already authenticated. Use this to switch accounts or re-authenticate. |
66
+ | `happy-mcp-server auth logout` | Remove saved credentials from `~/.happy-mcp/credentials.json`. |
67
+ | `happy-mcp-server help` | Display help message with available commands. |
52
68
 
53
69
  ### Transport Comparison
54
70
 
55
71
  | Feature | Stdio Transport | HTTP Transport |
56
72
  |---------|----------------|----------------|
57
73
  | **Use case** | MCP client integration (Claude Desktop, Cursor, etc.) | Custom clients, testing, or programmatic access |
58
- | **Command** | `happy-mcp` | `happy-mcp serve [--port <port>]` |
74
+ | **Command** | `happy-mcp-server` | `happy-mcp-server serve [--port <port>]` |
59
75
  | **Communication** | Standard input/output streams | HTTP POST/GET/DELETE to `/mcp` endpoint |
60
76
  | **Port** | N/A (uses stdio) | Auto-assigned or specified with `--port` |
61
77
  | **Accessibility** | Only via client process | HTTP endpoint on localhost (`127.0.0.1`) |
@@ -68,26 +84,29 @@ Environment variables customize server behavior:
68
84
  | Variable | Default | Description |
69
85
  |----------|---------|-------------|
70
86
  | `HAPPY_SERVER_URL` | `https://api.cluster-fluster.com` | Happy relay server URL. |
87
+ | `HAPPY_MCP_ACCOUNT_SECRET` | (none) | Base64-encoded 32-byte account secret for instant authentication. When set, bypasses credential file and QR pairing. Obtain via: `cat ~/.happy-mcp/credentials.json \| jq -r .secret` |
71
88
  | `HAPPY_MCP_COMPUTERS` | `os.hostname()` | Comma-separated list of computer hostnames to filter sessions and machines. Use `*` to show all computers. |
72
89
  | `HAPPY_MCP_PROJECT_PATHS` | `process.cwd()` | Comma-separated list of project path prefixes to filter sessions. Use `*` to show all paths. |
90
+ | `HAPPY_MCP_CREDENTIALS_PATH` | `~/.happy-mcp/credentials.json` | Path to credentials file |
73
91
  | `HAPPY_MCP_LOG_LEVEL` | `warn` | Log level: `debug`, `info`, `warn`, or `error`. Logs are written to stderr. |
92
+ | `HAPPY_MCP_SESSION_CACHE_TTL` | `300` | Session cache TTL in seconds |
74
93
  | `HAPPY_MCP_ENABLE_START` | `true` | Set to `false` to disable the `start_session` tool. |
75
94
 
76
95
  ## HTTP Transport
77
96
 
78
- When running `happy-mcp serve`, the server exposes MCP over HTTP instead of stdio. This mode is useful for custom clients, testing, or programmatic access.
97
+ 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.
79
98
 
80
99
  ### Starting the HTTP Server
81
100
 
82
101
  ```bash
83
102
  # Auto-assign port (reports actual port on startup)
84
- happy-mcp serve
103
+ happy-mcp-server serve
85
104
 
86
105
  # Specify a port
87
- happy-mcp serve --port 3000
106
+ happy-mcp-server serve --port 3000
88
107
 
89
108
  # With environment variables
90
- HAPPY_MCP_LOG_LEVEL=debug happy-mcp serve --port 8080
109
+ HAPPY_MCP_LOG_LEVEL=debug happy-mcp-server serve --port 8080
91
110
  ```
92
111
 
93
112
  ### Server Endpoint
@@ -110,7 +129,7 @@ http://127.0.0.1:<port>/mcp
110
129
 
111
130
  ### Requirements
112
131
 
113
- - **Authentication required**: You must run `happy-mcp auth` before starting the HTTP server. The server will exit with an error if credentials are not found.
132
+ - **Authentication required**: You must run `happy-mcp-server auth` or set `HAPPY_MCP_ACCOUNT_SECRET` before starting the HTTP server. The server will exit with an error if credentials are not found.
114
133
  - **Credential file permissions**: The credentials file must have `0600` permissions (readable only by owner).
115
134
 
116
135
  ### Example: Testing with curl
@@ -130,7 +149,7 @@ curl -X POST http://127.0.0.1:3000/mcp \
130
149
  Add to `.mcp.json` in your project root or configure via CLI:
131
150
 
132
151
  ```bash
133
- claude mcp add --transport stdio happy -- happy-mcp
152
+ claude mcp add --transport stdio happy -- happy-mcp-server
134
153
  ```
135
154
 
136
155
  Or manually add to `.mcp.json`:
@@ -140,7 +159,7 @@ Or manually add to `.mcp.json`:
140
159
  "mcpServers": {
141
160
  "happy": {
142
161
  "type": "stdio",
143
- "command": "happy-mcp"
162
+ "command": "happy-mcp-server"
144
163
  }
145
164
  }
146
165
  }
@@ -153,7 +172,7 @@ Or manually add to `.mcp.json`:
153
172
  "mcpServers": {
154
173
  "happy": {
155
174
  "type": "stdio",
156
- "command": "happy-mcp",
175
+ "command": "happy-mcp-server",
157
176
  "env": {
158
177
  "HAPPY_MCP_COMPUTERS": "*",
159
178
  "HAPPY_MCP_PROJECT_PATHS": "*"
@@ -178,7 +197,7 @@ Add to your Claude Desktop config:
178
197
  "mcpServers": {
179
198
  "happy": {
180
199
  "type": "stdio",
181
- "command": "happy-mcp"
200
+ "command": "happy-mcp-server"
182
201
  }
183
202
  }
184
203
  }
@@ -195,7 +214,7 @@ Add to `.cursor/mcp.json` in your project or global config:
195
214
  {
196
215
  "mcpServers": {
197
216
  "happy": {
198
- "command": "happy-mcp"
217
+ "command": "happy-mcp-server"
199
218
  }
200
219
  }
201
220
  }
@@ -212,7 +231,7 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
212
231
  {
213
232
  "mcpServers": {
214
233
  "happy": {
215
- "command": "happy-mcp"
234
+ "command": "happy-mcp-server"
216
235
  }
217
236
  }
218
237
  }
@@ -232,7 +251,7 @@ Add to `~/.continue/config.json`:
232
251
  {
233
252
  "transport": {
234
253
  "type": "stdio",
235
- "command": "happy-mcp"
254
+ "command": "happy-mcp-server"
236
255
  }
237
256
  }
238
257
  ]
@@ -0,0 +1,41 @@
1
+ import type { Credentials } from './crypto.js';
2
+ import type { Config } from '../config.js';
3
+ type PairingState = 'idle' | 'polling' | 'success';
4
+ export declare class PairingManager {
5
+ private config;
6
+ private onSuccess;
7
+ private session;
8
+ private state;
9
+ private pollTimerId;
10
+ private pollDeadline;
11
+ private isRequestInFlight;
12
+ private pairingComplete;
13
+ private pendingCredentials;
14
+ private destroyed;
15
+ private initiatePromise;
16
+ constructor(config: Config, onSuccess: (credentials: Credentials) => Promise<void>);
17
+ get currentState(): PairingState;
18
+ get hasSession(): boolean;
19
+ /**
20
+ * Initiate pairing: generate keypair, POST to relay, generate QR, start polling.
21
+ * If already initiated, restarts the polling timer and returns cached QR.
22
+ */
23
+ initiate(): Promise<{
24
+ qrAscii: string;
25
+ webUrl: string;
26
+ }>;
27
+ private _doInitiate;
28
+ /**
29
+ * Start background polling. Fire-and-forget.
30
+ * If already polling, restarts the deadline timer.
31
+ */
32
+ private startPolling;
33
+ private schedulePollTick;
34
+ private pollTick;
35
+ private pollOnce;
36
+ /**
37
+ * Clean up timers on shutdown.
38
+ */
39
+ destroy(): void;
40
+ }
41
+ export {};
@@ -0,0 +1,213 @@
1
+ import nacl from 'tweetnacl';
2
+ import axios from 'axios';
3
+ import qrcode from 'qrcode-terminal';
4
+ import { encodeBase64, encodeBase64Url, decodeBase64, decryptBoxBundle } from './crypto.js';
5
+ import { writeCredentials, readCredentials } from './credentials.js';
6
+ import { logger } from '../logger.js';
7
+ import { AuthError } from '../errors.js';
8
+ const POLL_INTERVAL = 1000;
9
+ const AUTH_TIMEOUT = 120_000;
10
+ export class PairingManager {
11
+ config;
12
+ onSuccess;
13
+ session = null;
14
+ state = 'idle';
15
+ pollTimerId = null;
16
+ pollDeadline = 0;
17
+ isRequestInFlight = false;
18
+ pairingComplete = false;
19
+ pendingCredentials = null;
20
+ destroyed = false;
21
+ initiatePromise = null;
22
+ constructor(config, onSuccess) {
23
+ this.config = config;
24
+ this.onSuccess = onSuccess;
25
+ }
26
+ get currentState() { return this.state; }
27
+ get hasSession() { return this.session !== null; }
28
+ /**
29
+ * Initiate pairing: generate keypair, POST to relay, generate QR, start polling.
30
+ * If already initiated, restarts the polling timer and returns cached QR.
31
+ */
32
+ async initiate() {
33
+ if (this.state === 'success') {
34
+ return { qrAscii: '', webUrl: '' }; // Already authenticated
35
+ }
36
+ // Coalesce concurrent calls
37
+ if (this.initiatePromise) {
38
+ return this.initiatePromise;
39
+ }
40
+ // Generate session if we don't have one
41
+ if (!this.session) {
42
+ this.initiatePromise = this._doInitiate();
43
+ try {
44
+ return await this.initiatePromise;
45
+ }
46
+ finally {
47
+ this.initiatePromise = null;
48
+ }
49
+ }
50
+ // Session already exists, restart polling
51
+ this.startPolling();
52
+ return { qrAscii: this.session.qrAscii, webUrl: this.session.webUrl };
53
+ }
54
+ async _doInitiate() {
55
+ const seed = nacl.randomBytes(32);
56
+ const keypair = nacl.box.keyPair.fromSecretKey(seed);
57
+ const publicKeyBase64 = encodeBase64(keypair.publicKey);
58
+ const publicKeyBase64Url = encodeBase64Url(keypair.publicKey);
59
+ // POST to relay
60
+ try {
61
+ await axios.post(`${this.config.serverUrl}/v1/auth/account/request`, {
62
+ publicKey: publicKeyBase64,
63
+ });
64
+ }
65
+ catch (err) {
66
+ if (axios.isAxiosError(err) && err.response) {
67
+ throw new AuthError(`Failed to initiate pairing: server returned ${err.response.status}`);
68
+ }
69
+ throw new AuthError('Failed to initiate pairing: network error');
70
+ }
71
+ // Generate ASCII QR code with timeout
72
+ const qrData = `happy:///account?${publicKeyBase64Url}`;
73
+ const qrPromise = new Promise((resolve) => {
74
+ qrcode.generate(qrData, { small: true }, (code) => {
75
+ resolve(code);
76
+ });
77
+ });
78
+ const timeoutPromise = new Promise((_, reject) => {
79
+ setTimeout(() => reject(new AuthError('QR code generation timed out')), 5000);
80
+ });
81
+ const qrAscii = await Promise.race([qrPromise, timeoutPromise]);
82
+ const webUrl = `https://app.happy.engineering/account/connect#key=${publicKeyBase64Url}`;
83
+ this.session = { keypair, publicKeyBase64, publicKeyBase64Url, qrAscii, webUrl };
84
+ this.startPolling();
85
+ return { qrAscii: this.session.qrAscii, webUrl: this.session.webUrl };
86
+ }
87
+ /**
88
+ * Start background polling. Fire-and-forget.
89
+ * If already polling, restarts the deadline timer.
90
+ */
91
+ startPolling() {
92
+ if (this.destroyed)
93
+ return;
94
+ // Clear existing timer
95
+ if (this.pollTimerId !== null) {
96
+ clearTimeout(this.pollTimerId);
97
+ this.pollTimerId = null;
98
+ }
99
+ this.state = 'polling';
100
+ this.pollDeadline = Date.now() + AUTH_TIMEOUT;
101
+ this.schedulePollTick();
102
+ }
103
+ schedulePollTick() {
104
+ this.pollTimerId = setTimeout(() => {
105
+ this.pollTick();
106
+ }, POLL_INTERVAL);
107
+ }
108
+ async pollTick() {
109
+ if (this.destroyed)
110
+ return;
111
+ // Check deadline
112
+ if (Date.now() > this.pollDeadline) {
113
+ this.state = 'idle'; // expired but session/QR preserved
114
+ this.pollTimerId = null;
115
+ logger.debug('Pairing poll deadline expired');
116
+ return;
117
+ }
118
+ // Don't overlap with in-flight request — wait for next interval
119
+ if (this.isRequestInFlight) {
120
+ this.pollTimerId = setTimeout(() => this.pollTick(), POLL_INTERVAL);
121
+ return;
122
+ }
123
+ this.isRequestInFlight = true;
124
+ try {
125
+ const authorized = await this.pollOnce();
126
+ if (authorized) {
127
+ return; // Success handled in pollOnce
128
+ }
129
+ }
130
+ catch (err) {
131
+ logger.debug('Pairing poll error:', err.message);
132
+ }
133
+ finally {
134
+ this.isRequestInFlight = false;
135
+ }
136
+ // Schedule next tick if still within deadline and still polling
137
+ if (this.state === 'polling' && Date.now() < this.pollDeadline) {
138
+ this.schedulePollTick();
139
+ }
140
+ }
141
+ async pollOnce() {
142
+ if (this.destroyed)
143
+ return false;
144
+ if (!this.session)
145
+ return false;
146
+ // Bug C2 fix: If pairing already completed, only retry activation
147
+ if (this.pairingComplete && this.pendingCredentials) {
148
+ try {
149
+ await this.onSuccess(this.pendingCredentials);
150
+ }
151
+ catch (err) {
152
+ logger.error('Activation retry failed:', err.message);
153
+ return false;
154
+ }
155
+ this.state = 'success';
156
+ this.pollTimerId = null;
157
+ this.session = null;
158
+ this.pendingCredentials = null;
159
+ logger.info('Pairing successful via MCP tool (retry)');
160
+ return true;
161
+ }
162
+ const res = await axios.post(`${this.config.serverUrl}/v1/auth/account/request`, {
163
+ publicKey: this.session.publicKeyBase64,
164
+ });
165
+ if (res.data.state === 'authorized') {
166
+ // Decrypt the account secret
167
+ const encryptedResponse = decodeBase64(res.data.response);
168
+ const accountSecret = decryptBoxBundle(encryptedResponse, this.session.keypair.secretKey);
169
+ if (!accountSecret) {
170
+ throw new AuthError('Failed to decrypt pairing response');
171
+ }
172
+ // Write credentials to disk
173
+ writeCredentials(this.config.credentialsPath, res.data.token, accountSecret, this.config.serverUrl);
174
+ // Read back to get full Credentials object
175
+ const creds = readCredentials(this.config.credentialsPath);
176
+ if (!creds) {
177
+ throw new AuthError('Failed to read back written credentials');
178
+ }
179
+ // Mark pairing as complete BEFORE calling onSuccess
180
+ this.pairingComplete = true;
181
+ this.pendingCredentials = creds;
182
+ // Fire callback FIRST (this triggers tool activation)
183
+ // State is set to 'success' only after onSuccess completes,
184
+ // so a failure leaves state as 'polling' and allows retry.
185
+ try {
186
+ await this.onSuccess(creds);
187
+ }
188
+ catch (err) {
189
+ logger.error('Pairing succeeded but activation failed:', err.message);
190
+ return false;
191
+ }
192
+ this.state = 'success';
193
+ this.pollTimerId = null;
194
+ this.session = null; // Clear keypair from memory
195
+ this.pendingCredentials = null;
196
+ logger.info('Pairing successful via MCP tool');
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+ /**
202
+ * Clean up timers on shutdown.
203
+ */
204
+ destroy() {
205
+ this.destroyed = true;
206
+ if (this.pollTimerId !== null) {
207
+ clearTimeout(this.pollTimerId);
208
+ this.pollTimerId = null;
209
+ }
210
+ this.state = 'idle'; // Bug C1 fix: prevent in-flight requests from rescheduling
211
+ this.session = null; // Clear keypair from memory
212
+ }
213
+ }
@@ -20,7 +20,7 @@ export async function loadOrPairCredentials(config) {
20
20
  logger.info('Loaded existing credentials');
21
21
  return creds;
22
22
  }
23
- console.error('[happy-mcp] No credentials found. Starting pairing flow...');
23
+ console.error('[happy-mcp-server] No credentials found. Starting pairing flow...');
24
24
  return performPairing(config);
25
25
  }
26
26
  /**
@@ -72,7 +72,7 @@ export async function performPairing(config) {
72
72
  }
73
73
  // 6. Persist credentials
74
74
  writeCredentials(config.credentialsPath, res.data.token, accountSecret, config.serverUrl);
75
- console.error('[happy-mcp] Paired successfully!');
75
+ console.error('[happy-mcp-server] Paired successfully!');
76
76
  const creds = readCredentials(config.credentialsPath);
77
77
  if (!creds) {
78
78
  throw new AuthError('Failed to read back written credentials');
@@ -0,0 +1,12 @@
1
+ import type { Credentials } from './crypto.js';
2
+ /**
3
+ * Authenticate from a raw account secret (32 bytes) via Ed25519 challenge-response.
4
+ * This is used for environment-variable-based auth in ephemeral environments.
5
+ *
6
+ * Endpoint: POST /v1/auth
7
+ * Body: { challenge: base64, publicKey: base64, signature: base64 }
8
+ * Response: { success: boolean, token: string }
9
+ *
10
+ * Returns in-memory Credentials object (does NOT write to disk).
11
+ */
12
+ export declare function authenticateFromSecret(secret: Uint8Array, serverUrl: string): Promise<Credentials>;
@@ -0,0 +1,40 @@
1
+ import axios from 'axios';
2
+ import { authChallenge, encodeBase64, deriveContentKeyPair } from './crypto.js';
3
+ import { logger } from '../logger.js';
4
+ import { AuthError } from '../errors.js';
5
+ /**
6
+ * Authenticate from a raw account secret (32 bytes) via Ed25519 challenge-response.
7
+ * This is used for environment-variable-based auth in ephemeral environments.
8
+ *
9
+ * Endpoint: POST /v1/auth
10
+ * Body: { challenge: base64, publicKey: base64, signature: base64 }
11
+ * Response: { success: boolean, token: string }
12
+ *
13
+ * Returns in-memory Credentials object (does NOT write to disk).
14
+ */
15
+ export async function authenticateFromSecret(secret, serverUrl) {
16
+ logger.info('Authenticating via HAPPY_MCP_ACCOUNT_SECRET...');
17
+ const { challenge, publicKey, signature } = authChallenge(secret);
18
+ try {
19
+ const res = await axios.post(`${serverUrl}/v1/auth`, {
20
+ challenge: encodeBase64(challenge),
21
+ publicKey: encodeBase64(publicKey),
22
+ signature: encodeBase64(signature),
23
+ });
24
+ if (!res.data.success || !res.data.token) {
25
+ throw new AuthError('Secret auth failed: server returned unsuccessful response');
26
+ }
27
+ const token = res.data.token;
28
+ const contentKeyPair = deriveContentKeyPair(secret);
29
+ logger.info('Secret authentication successful');
30
+ return { token, secret, contentKeyPair };
31
+ }
32
+ catch (err) {
33
+ if (err instanceof AuthError)
34
+ throw err;
35
+ if (axios.isAxiosError(err) && err.response) {
36
+ throw new AuthError(`Secret auth failed: server returned ${err.response.status}`);
37
+ }
38
+ throw new AuthError('Secret auth failed: network error');
39
+ }
40
+ }
package/dist/config.d.ts CHANGED
@@ -7,5 +7,6 @@ export interface Config {
7
7
  logLevel: LogLevel;
8
8
  sessionCacheTtl: number;
9
9
  enableStart: boolean;
10
+ accountSecret: Uint8Array | null;
10
11
  }
11
12
  export declare function loadConfig(): Config;
package/dist/config.js CHANGED
@@ -9,5 +9,22 @@ export function loadConfig() {
9
9
  const logLevel = (process.env.HAPPY_MCP_LOG_LEVEL ?? 'warn');
10
10
  const sessionCacheTtl = parseInt(process.env.HAPPY_MCP_SESSION_CACHE_TTL ?? '300', 10);
11
11
  const enableStart = process.env.HAPPY_MCP_ENABLE_START !== 'false';
12
- return { serverUrl, computers, projectPaths, credentialsPath, logLevel, sessionCacheTtl, enableStart };
12
+ // Parse HAPPY_MCP_ACCOUNT_SECRET env var (non-fatal: warn and ignore if invalid)
13
+ const accountSecretEnv = process.env.HAPPY_MCP_ACCOUNT_SECRET;
14
+ let accountSecret = null;
15
+ if (accountSecretEnv) {
16
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(accountSecretEnv)) {
17
+ console.error('[happy-mcp-server] WARNING: HAPPY_MCP_ACCOUNT_SECRET is not valid base64, ignoring');
18
+ }
19
+ else {
20
+ const decoded = Buffer.from(accountSecretEnv, 'base64');
21
+ if (decoded.length !== 32) {
22
+ console.error('[happy-mcp-server] WARNING: HAPPY_MCP_ACCOUNT_SECRET is not 32 bytes, ignoring');
23
+ }
24
+ else {
25
+ accountSecret = new Uint8Array(decoded);
26
+ }
27
+ }
28
+ }
29
+ return { serverUrl, computers, projectPaths, credentialsPath, logLevel, sessionCacheTtl, enableStart, accountSecret };
13
30
  }
package/dist/http.js CHANGED
@@ -52,7 +52,7 @@ export async function startHttpServer(config, api, relay, sessionManager, port =
52
52
  };
53
53
  // Create a new McpServer instance for this session
54
54
  const server = new McpServer({
55
- name: 'happy-mcp',
55
+ name: 'happy-mcp-server',
56
56
  version: '0.1.0',
57
57
  });
58
58
  // Register all tools on this server instance
package/dist/index.js CHANGED
@@ -4,12 +4,14 @@ import { loadConfig } from './config.js';
4
4
  import { setupLogger, logger } from './logger.js';
5
5
  import { performPairing } from './auth/pairing.js';
6
6
  import { readCredentials, validateFilePermissions, clearCredentials } from './auth/credentials.js';
7
+ import { authenticateFromSecret } from './auth/secret-auth.js';
7
8
  import { decryptFromBase64 } from './auth/crypto.js';
8
9
  import { resolveSessionEncryptionCached, clearKeyCache } from './session/keys.js';
9
10
  import { SessionManager } from './session/manager.js';
10
11
  import { ApiClient } from './api/client.js';
11
12
  import { RelayClient } from './relay/client.js';
12
13
  import { createUnauthenticatedServer } from './server.js';
14
+ import { PairingManager } from './auth/pairing-manager.js';
13
15
  import { startHttpServer } from './http.js';
14
16
  // ---------------------------------------------------------------------------
15
17
  // Helpers
@@ -152,7 +154,7 @@ async function initializeAuthenticatedState(config, credentials) {
152
154
  // CLI Commands
153
155
  // ---------------------------------------------------------------------------
154
156
  function printHelp() {
155
- console.error(`Usage: happy-mcp [command] [options]
157
+ console.error(`Usage: happy-mcp-server [command] [options]
156
158
 
157
159
  Commands:
158
160
  (no args) Start the MCP server (stdio transport)
@@ -172,19 +174,19 @@ function parseServeFlags(argv) {
172
174
  if (argv[i] === '--port') {
173
175
  const raw = argv[i + 1];
174
176
  if (raw === undefined || raw.startsWith('-')) {
175
- console.error('[happy-mcp] --port requires a port number. Example: happy-mcp serve --port 3000');
177
+ console.error('[happy-mcp-server] --port requires a port number. Example: happy-mcp-server serve --port 3000');
176
178
  process.exit(1);
177
179
  }
178
180
  const parsed = Number(raw);
179
181
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
180
- console.error(`[happy-mcp] Invalid port '${raw}'. Must be an integer between 1 and 65535.`);
182
+ console.error(`[happy-mcp-server] Invalid port '${raw}'. Must be an integer between 1 and 65535.`);
181
183
  process.exit(1);
182
184
  }
183
185
  port = parsed;
184
186
  i++;
185
187
  }
186
188
  else {
187
- console.error(`[happy-mcp] Unknown flag '${argv[i]}'. Run \`happy-mcp help\` for help.`);
189
+ console.error(`[happy-mcp-server] Unknown flag '${argv[i]}'. Run \`happy-mcp-server help\` for help.`);
188
190
  process.exit(1);
189
191
  }
190
192
  }
@@ -194,9 +196,9 @@ async function runAuth() {
194
196
  const config = loadConfig();
195
197
  const creds = readCredentials(config.credentialsPath);
196
198
  if (creds) {
197
- console.error('[happy-mcp] Already authenticated.');
198
- console.error(' To login with a different account: happy-mcp auth login');
199
- console.error(' To logout: happy-mcp auth logout');
199
+ console.error('[happy-mcp-server] Already authenticated.');
200
+ console.error(' To login with a different account: happy-mcp-server auth login');
201
+ console.error(' To logout: happy-mcp-server auth logout');
200
202
  process.exit(0);
201
203
  }
202
204
  await runAuthLogin();
@@ -205,7 +207,7 @@ async function runAuthLogin() {
205
207
  const config = loadConfig();
206
208
  setupLogger('info');
207
209
  await performPairing(config);
208
- console.error('[happy-mcp] Authentication complete.');
210
+ console.error('[happy-mcp-server] Authentication complete.');
209
211
  process.exit(0);
210
212
  }
211
213
  async function runAuthLogout() {
@@ -213,12 +215,12 @@ async function runAuthLogout() {
213
215
  setupLogger(config.logLevel);
214
216
  const creds = readCredentials(config.credentialsPath);
215
217
  if (!creds) {
216
- console.error('[happy-mcp] Not currently authenticated. Nothing to do.');
218
+ console.error('[happy-mcp-server] Not currently authenticated. Nothing to do.');
217
219
  process.exit(0);
218
220
  }
219
221
  clearCredentials(config.credentialsPath);
220
- console.error('[happy-mcp] Logged out. Credentials removed.');
221
- console.error(' To re-authenticate: happy-mcp auth login');
222
+ console.error('[happy-mcp-server] Logged out. Credentials removed.');
223
+ console.error(' To re-authenticate: happy-mcp-server auth login');
222
224
  process.exit(0);
223
225
  }
224
226
  // ---------------------------------------------------------------------------
@@ -227,7 +229,7 @@ async function runAuthLogout() {
227
229
  async function runServer() {
228
230
  const config = loadConfig();
229
231
  setupLogger(config.logLevel);
230
- logger.info('Starting happy-mcp...');
232
+ logger.info('Starting happy-mcp-server...');
231
233
  const authState = {
232
234
  authenticated: false,
233
235
  activatingPromise: null,
@@ -235,33 +237,30 @@ async function runServer() {
235
237
  sessionManager: null,
236
238
  };
237
239
  let activateFn = null;
238
- async function tryActivate() {
240
+ /**
241
+ * Shared activation logic: initialize authenticated state and swap in real tools.
242
+ * All auth paths (env var, file-based, MCP pairing) converge here.
243
+ */
244
+ async function performActivation(credentials) {
239
245
  if (authState.authenticated)
240
246
  return true;
241
247
  if (authState.activatingPromise)
242
248
  return authState.activatingPromise;
243
- const creds = readCredentials(config.credentialsPath);
244
- if (!creds)
245
- return false;
246
- try {
247
- validateFilePermissions(config.credentialsPath);
248
- }
249
- catch (err) {
250
- logger.debug('Credential file has unsafe permissions:', err.message);
251
- return false;
252
- }
253
249
  authState.activatingPromise = (async () => {
254
250
  try {
255
- const state = await initializeAuthenticatedState(config, creds);
251
+ const state = await initializeAuthenticatedState(config, credentials);
252
+ if (!activateFn) {
253
+ logger.error('activate function not ready yet');
254
+ return false;
255
+ }
256
+ activateFn(state.api, state.relay, state.sessionManager);
256
257
  authState.relay = state.relay;
257
258
  authState.sessionManager = state.sessionManager;
258
- activateFn(state.api, state.relay, state.sessionManager);
259
259
  authState.authenticated = true;
260
- logger.info('Lazy authentication complete -- tools are now active');
261
260
  return true;
262
261
  }
263
262
  catch (err) {
264
- logger.debug('Lazy auth failed:', err.message);
263
+ logger.debug('Activation failed:', err.message);
265
264
  return false;
266
265
  }
267
266
  finally {
@@ -270,31 +269,78 @@ async function runServer() {
270
269
  })();
271
270
  return authState.activatingPromise;
272
271
  }
273
- const { server, activate } = createUnauthenticatedServer(config, tryActivate);
272
+ async function tryActivate() {
273
+ if (authState.authenticated)
274
+ return true;
275
+ if (authState.activatingPromise)
276
+ return authState.activatingPromise;
277
+ const creds = readCredentials(config.credentialsPath);
278
+ if (!creds)
279
+ return false;
280
+ try {
281
+ validateFilePermissions(config.credentialsPath);
282
+ }
283
+ catch (err) {
284
+ logger.debug('Credential file has unsafe permissions:', err.message);
285
+ return false;
286
+ }
287
+ const result = await performActivation(creds);
288
+ if (result)
289
+ logger.info('Lazy authentication complete -- tools are now active');
290
+ return result;
291
+ }
292
+ // Create PairingManager for in-process QR pairing
293
+ const pairingManager = new PairingManager(config, async (credentials) => {
294
+ const result = await performActivation(credentials);
295
+ if (result) {
296
+ logger.info('MCP-based authentication complete — tools are now active');
297
+ }
298
+ else {
299
+ throw new Error('Activation failed after successful pairing');
300
+ }
301
+ });
302
+ const { server, activate } = createUnauthenticatedServer(config, tryActivate, pairingManager);
274
303
  activateFn = activate;
275
304
  // Connect stdio transport FIRST (Issue 1: prevent blocking)
276
305
  const transport = new StdioServerTransport();
277
306
  await server.connect(transport);
278
307
  logger.info(`MCP server started — credentials path: ${config.credentialsPath}`);
279
- // Then initialize auth state via tryActivate() only (consolidate startup path)
280
- const initialCreds = readCredentials(config.credentialsPath);
281
- if (initialCreds) {
308
+ // Priority 1: HAPPY_MCP_ACCOUNT_SECRET env var
309
+ if (config.accountSecret) {
282
310
  try {
283
- validateFilePermissions(config.credentialsPath);
284
- logger.info('Credentials found at startup, initializing...');
285
- await tryActivate();
311
+ const creds = await authenticateFromSecret(config.accountSecret, config.serverUrl);
312
+ await performActivation(creds);
313
+ if (authState.authenticated) {
314
+ logger.info('Authenticated via HAPPY_MCP_ACCOUNT_SECRET');
315
+ }
286
316
  }
287
317
  catch (err) {
288
- logger.debug('Failed to initialize with existing credentials:', err.message);
318
+ logger.warn('HAPPY_MCP_ACCOUNT_SECRET auth failed:', err.message);
319
+ // Fall through to file-based / QR auth
289
320
  }
290
321
  }
291
- else {
292
- logger.debug('No credentials found. Tools will attempt auth on each call.');
293
- logger.debug('Run `happy-mcp auth` to authenticate.');
322
+ // Priority 2: Existing credentials file
323
+ if (!authState.authenticated) {
324
+ const initialCreds = readCredentials(config.credentialsPath);
325
+ if (initialCreds) {
326
+ try {
327
+ validateFilePermissions(config.credentialsPath);
328
+ logger.info('Credentials found at startup, initializing...');
329
+ await tryActivate();
330
+ }
331
+ catch (err) {
332
+ logger.debug('Failed to initialize with existing credentials:', err.message);
333
+ }
334
+ }
335
+ else {
336
+ logger.debug('No credentials found. Tools will attempt auth on each call.');
337
+ logger.debug('Run `happy-mcp-server auth` to authenticate.');
338
+ }
294
339
  }
295
340
  logger.info(`MCP server started (${authState.authenticated ? 'authenticated' : 'unauthenticated'} mode)`);
296
341
  const shutdown = async () => {
297
342
  logger.info('Shutting down...');
343
+ pairingManager.destroy();
298
344
  authState.relay?.disconnect();
299
345
  authState.sessionManager?.destroy();
300
346
  clearKeyCache();
@@ -318,19 +364,35 @@ async function runServer() {
318
364
  async function runHttpServer(port) {
319
365
  const config = loadConfig();
320
366
  setupLogger(config.logLevel);
321
- const creds = readCredentials(config.credentialsPath);
322
- if (!creds) {
323
- console.error('[happy-mcp] No credentials found. Run `happy-mcp auth` first.');
324
- process.exit(1);
367
+ // Priority 1: HAPPY_MCP_ACCOUNT_SECRET env var
368
+ let creds = null;
369
+ if (config.accountSecret) {
370
+ try {
371
+ creds = await authenticateFromSecret(config.accountSecret, config.serverUrl);
372
+ logger.info('HTTP server authenticated via HAPPY_MCP_ACCOUNT_SECRET');
373
+ }
374
+ catch (err) {
375
+ logger.warn('HAPPY_MCP_ACCOUNT_SECRET auth failed:', err.message);
376
+ }
325
377
  }
326
- try {
327
- validateFilePermissions(config.credentialsPath);
378
+ // Priority 2: Credentials file
379
+ if (!creds) {
380
+ creds = readCredentials(config.credentialsPath);
381
+ if (creds) {
382
+ try {
383
+ validateFilePermissions(config.credentialsPath);
384
+ }
385
+ catch (err) {
386
+ console.error('[happy-mcp-server] Credential file has unsafe permissions:', err.message);
387
+ process.exit(1);
388
+ }
389
+ }
328
390
  }
329
- catch (err) {
330
- console.error('[happy-mcp] Credential file has unsafe permissions:', err.message);
391
+ if (!creds) {
392
+ console.error('[happy-mcp-server] No credentials found. Run `happy-mcp-server auth` or set HAPPY_MCP_ACCOUNT_SECRET.');
331
393
  process.exit(1);
332
394
  }
333
- logger.info('Starting happy-mcp HTTP server...');
395
+ logger.info('Starting happy-mcp-server HTTP server...');
334
396
  const state = await initializeAuthenticatedState(config, creds);
335
397
  let result;
336
398
  try {
@@ -339,17 +401,17 @@ async function runHttpServer(port) {
339
401
  catch (error) {
340
402
  const err = error;
341
403
  if (err.code === 'EADDRINUSE') {
342
- console.error(`[happy-mcp] Port ${port} is already in use.`);
404
+ console.error(`[happy-mcp-server] Port ${port} is already in use.`);
343
405
  }
344
406
  else if (err.code === 'EACCES') {
345
- console.error(`[happy-mcp] Permission denied binding to port ${port}. Try a port above 1023.`);
407
+ console.error(`[happy-mcp-server] Permission denied binding to port ${port}. Try a port above 1023.`);
346
408
  }
347
409
  else {
348
- console.error(`[happy-mcp] Failed to start HTTP server: ${err.message ?? err}`);
410
+ console.error(`[happy-mcp-server] Failed to start HTTP server: ${err.message ?? err}`);
349
411
  }
350
412
  process.exit(1);
351
413
  }
352
- console.error(`[happy-mcp] HTTP server listening on http://127.0.0.1:${result.port}/mcp`);
414
+ console.error(`[happy-mcp-server] HTTP server listening on http://127.0.0.1:${result.port}/mcp`);
353
415
  const shutdown = async () => {
354
416
  logger.info('Shutting down HTTP server...');
355
417
  await result.close();
@@ -367,14 +429,14 @@ async function runHttpServer(port) {
367
429
  const command = process.argv[2];
368
430
  if (!command) {
369
431
  runServer().catch((err) => {
370
- console.error('[happy-mcp] Fatal:', err.message ?? err);
432
+ console.error('[happy-mcp-server] Fatal:', err.message ?? err);
371
433
  process.exit(1);
372
434
  });
373
435
  }
374
436
  else if (command === 'serve') {
375
437
  const flags = parseServeFlags(process.argv.slice(3));
376
438
  runHttpServer(flags.port).catch((err) => {
377
- console.error('[happy-mcp] Fatal:', err.message ?? err);
439
+ console.error('[happy-mcp-server] Fatal:', err.message ?? err);
378
440
  process.exit(1);
379
441
  });
380
442
  }
@@ -382,24 +444,24 @@ else if (command === 'auth') {
382
444
  const subcommand = process.argv[3];
383
445
  if (!subcommand) {
384
446
  runAuth().catch((err) => {
385
- console.error('[happy-mcp] Fatal:', err.message ?? err);
447
+ console.error('[happy-mcp-server] Fatal:', err.message ?? err);
386
448
  process.exit(1);
387
449
  });
388
450
  }
389
451
  else if (subcommand === 'login') {
390
452
  runAuthLogin().catch((err) => {
391
- console.error('[happy-mcp] Fatal:', err.message ?? err);
453
+ console.error('[happy-mcp-server] Fatal:', err.message ?? err);
392
454
  process.exit(1);
393
455
  });
394
456
  }
395
457
  else if (subcommand === 'logout') {
396
458
  runAuthLogout().catch((err) => {
397
- console.error('[happy-mcp] Fatal:', err.message ?? err);
459
+ console.error('[happy-mcp-server] Fatal:', err.message ?? err);
398
460
  process.exit(1);
399
461
  });
400
462
  }
401
463
  else {
402
- console.error(`[happy-mcp] Unknown auth command '${subcommand}'. Run \`happy-mcp help\` for help.`);
464
+ console.error(`[happy-mcp-server] Unknown auth command '${subcommand}'. Run \`happy-mcp-server help\` for help.`);
403
465
  process.exit(1);
404
466
  }
405
467
  }
@@ -408,6 +470,6 @@ else if (command === 'help') {
408
470
  process.exit(0);
409
471
  }
410
472
  else {
411
- console.error(`[happy-mcp] Unknown command '${command}'. Run \`happy-mcp help\` for help.`);
473
+ console.error(`[happy-mcp-server] Unknown command '${command}'. Run \`happy-mcp-server help\` for help.`);
412
474
  process.exit(1);
413
475
  }
package/dist/logger.js CHANGED
@@ -10,7 +10,7 @@ export function setupLogger(level) {
10
10
  }
11
11
  function log(level, ...args) {
12
12
  if (LEVELS[level] >= currentLevel) {
13
- const prefix = `[happy-mcp] [${level.toUpperCase()}]`;
13
+ const prefix = `[happy-mcp-server] [${level.toUpperCase()}]`;
14
14
  console.error(prefix, ...args);
15
15
  }
16
16
  }
package/dist/server.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { ApiClient } from './api/client.js';
3
3
  import type { RelayClient } from './relay/client.js';
4
4
  import type { SessionManager } from './session/manager.js';
5
5
  import type { Config } from './config.js';
6
+ import type { PairingManager } from './auth/pairing-manager.js';
6
7
  /**
7
8
  * Register all real tool handlers on an McpServer instance.
8
9
  * Used by both stdio (via activate()) and HTTP (directly).
@@ -18,4 +19,4 @@ export interface UnauthenticatedServer {
18
19
  * The callback should attempt authentication and return true on success.
19
20
  * Call activate() to swap in real tool handlers.
20
21
  */
21
- export declare function createUnauthenticatedServer(config: Config, onToolCallWhileUnauthenticated: () => Promise<boolean>): UnauthenticatedServer;
22
+ export declare function createUnauthenticatedServer(config: Config, onToolCallWhileUnauthenticated: () => Promise<boolean>, pairingManager?: PairingManager): UnauthenticatedServer;
package/dist/server.js CHANGED
@@ -13,7 +13,7 @@ const AUTH_ERROR = {
13
13
  isError: true,
14
14
  content: [{
15
15
  type: 'text',
16
- text: 'Not authenticated. User must run `happy-mcp auth` in their terminal to authenticate before this tool is available. Have the user run this command and then try again.',
16
+ text: 'Not authenticated. Call the `authentication_status` tool to begin authentication.',
17
17
  }],
18
18
  };
19
19
  const AUTH_RETRY = {
@@ -57,15 +57,16 @@ export function registerAllTools(server, config, api, relay, sessionManager) {
57
57
  * The callback should attempt authentication and return true on success.
58
58
  * Call activate() to swap in real tool handlers.
59
59
  */
60
- export function createUnauthenticatedServer(config, onToolCallWhileUnauthenticated) {
60
+ export function createUnauthenticatedServer(config, onToolCallWhileUnauthenticated, pairingManager) {
61
61
  const server = new McpServer({
62
- name: 'happy-mcp',
62
+ name: 'happy-mcp-server',
63
63
  version: '0.1.0',
64
64
  });
65
65
  let stubRegistrations = [];
66
+ let activated = false;
66
67
  const stubHandler = async () => {
67
- const activated = await onToolCallWhileUnauthenticated();
68
- if (activated) {
68
+ const didActivate = await onToolCallWhileUnauthenticated();
69
+ if (didActivate) {
69
70
  return AUTH_RETRY;
70
71
  }
71
72
  return AUTH_ERROR;
@@ -81,10 +82,56 @@ export function createUnauthenticatedServer(config, onToolCallWhileUnauthenticat
81
82
  }
82
83
  // Start with stubs
83
84
  registerStubs();
85
+ // Register authentication_status tool OUTSIDE of stubRegistrations
86
+ // so it is NOT removed by activate()
87
+ if (pairingManager) {
88
+ server.tool('authentication_status', 'Check authentication status. If not authenticated, returns a QR code to scan with the Happy mobile app. Display the QR code to the user. Background polling will automatically detect when authentication completes.', {}, async () => {
89
+ if (activated || pairingManager.currentState === 'success') {
90
+ return {
91
+ content: [{
92
+ type: 'text',
93
+ text: 'Authenticated. All tools are active.',
94
+ }],
95
+ };
96
+ }
97
+ try {
98
+ const { qrAscii, webUrl } = await pairingManager.initiate();
99
+ return {
100
+ content: [
101
+ {
102
+ type: 'text',
103
+ text: qrAscii,
104
+ },
105
+ {
106
+ type: 'text',
107
+ text: [
108
+ 'Scan the QR code above with the Happy mobile app to authenticate.',
109
+ '',
110
+ `Or visit: ${webUrl}`,
111
+ '',
112
+ 'Authentication will complete automatically once scanned.',
113
+ 'You can then retry your previous request.',
114
+ ].join('\n'),
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ catch (err) {
120
+ return {
121
+ isError: true,
122
+ content: [{
123
+ type: 'text',
124
+ text: `Authentication failed: ${err.message}`,
125
+ }],
126
+ };
127
+ }
128
+ });
129
+ }
84
130
  return {
85
131
  server,
86
132
  activate(api, relay, sessionManager) {
87
- // Remove all stubs synchronously
133
+ activated = true;
134
+ // Remove all stubs synchronously (authentication_status is NOT in this array)
88
135
  for (const stub of stubRegistrations) {
89
136
  stub.remove();
90
137
  }
@@ -45,7 +45,7 @@ export function registerAnswerQuestion(server, api, relay, sessionManager) {
45
45
  const content = {
46
46
  role: 'user',
47
47
  content: { type: 'text', text: answerText },
48
- meta: { sentFrom: 'happy-mcp' },
48
+ meta: { sentFrom: 'happy-mcp-server' },
49
49
  };
50
50
  const encrypted = encryptToBase64(session.encryption, content);
51
51
  await api.sendMessages(sessionId, [{ content: encrypted, localId: randomUUID() }]);
@@ -31,7 +31,7 @@ export function registerSendMessage(server, relay, sessionManager) {
31
31
  role: 'user',
32
32
  content: { type: 'text', text: message },
33
33
  meta: {
34
- sentFrom: 'happy-mcp',
34
+ sentFrom: 'happy-mcp-server',
35
35
  ...(meta?.permissionMode ? { permissionMode: meta.permissionMode } : {}),
36
36
  ...(meta?.allowedTools ? { allowedTools: meta.allowedTools } : {}),
37
37
  ...(meta?.disallowedTools ? { disallowedTools: meta.disallowedTools } : {}),
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  const DEFAULT_TIMEOUT_SECONDS = 300; // 5 minutes
3
- const MAX_WAIT_MS = 30_000; // 30s, safely under 60s MCP timeout
3
+ const MAX_WAIT_MS = 300_000; // 5 min max wait
4
4
  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 30 seconds for a state change. If sessions are still active, returns current status -- call again to continue watching.', {
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
6
  sessionIds: z.array(z.string()).describe('One or more session IDs to watch'),
7
7
  timeoutSeconds: z.number().optional().default(DEFAULT_TIMEOUT_SECONDS).describe('Max wait time in seconds (default 300)'),
8
8
  }, async ({ sessionIds, timeoutSeconds }, extra) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-mcp-server",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "MCP server for observing and controlling Happy Coder sessions",
5
5
  "author": {
6
6
  "name": "Jared Spencer",
@@ -31,7 +31,7 @@
31
31
  "type": "module",
32
32
  "main": "dist/index.js",
33
33
  "bin": {
34
- "happy-mcp": "dist/index.js"
34
+ "happy-mcp-server": "dist/index.js"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc && chmod +x dist/index.js",