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 +45 -26
- package/dist/auth/pairing-manager.d.ts +41 -0
- package/dist/auth/pairing-manager.js +213 -0
- package/dist/auth/pairing.js +2 -2
- package/dist/auth/secret-auth.d.ts +12 -0
- package/dist/auth/secret-auth.js +40 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +18 -1
- package/dist/http.js +1 -1
- package/dist/index.js +120 -58
- package/dist/logger.js +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +66 -18
- package/dist/tools/answer_question.js +3 -7
- package/dist/tools/approve_permission.js +2 -10
- package/dist/tools/deny_permission.js +2 -7
- package/dist/tools/get_session.js +2 -7
- package/dist/tools/interrupt_session.js +2 -4
- package/dist/tools/list_computers.js +2 -1
- package/dist/tools/list_sessions.js +2 -8
- package/dist/tools/schemas.d.ts +83 -0
- package/dist/tools/schemas.js +81 -0
- package/dist/tools/send_message.js +3 -12
- package/dist/tools/start_session.js +6 -7
- package/dist/tools/watch_session.js +3 -7
- package/package.json +2 -2
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
|
|
32
|
+
export HAPPY_MCP_ACCOUNT_SECRET=$(cat ~/.happy-mcp/credentials.json | jq -r .secret)
|
|
17
33
|
```
|
|
18
34
|
|
|
19
|
-
|
|
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
|
+
}
|
package/dist/auth/pairing.js
CHANGED
|
@@ -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
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
|
-
|
|
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
|