happy-mcp-server 1.1.0 → 1.1.2

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