@phantom/mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1448 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ SessionManager: () => SessionManager
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // src/server.ts
39
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
40
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
41
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
42
+
43
+ // src/session/manager.ts
44
+ var import_client = require("@phantom/client");
45
+ var import_api_key_stamper = require("@phantom/api-key-stamper");
46
+
47
+ // src/session/storage.ts
48
+ var fs = __toESM(require("fs"));
49
+ var path = __toESM(require("path"));
50
+ var os = __toESM(require("os"));
51
+ var SessionStorage = class {
52
+ constructor(sessionDir) {
53
+ this.sessionDir = sessionDir || path.join(os.homedir(), ".phantom-mcp");
54
+ this.sessionFile = path.join(this.sessionDir, "session.json");
55
+ }
56
+ /**
57
+ * Ensures session directory exists with secure permissions (0o700)
58
+ */
59
+ ensureSessionDir() {
60
+ try {
61
+ fs.mkdirSync(this.sessionDir, { mode: 448, recursive: true });
62
+ } catch (error) {
63
+ const err = error;
64
+ if (err.code !== "EEXIST") {
65
+ throw error;
66
+ }
67
+ }
68
+ try {
69
+ fs.chmodSync(this.sessionDir, 448);
70
+ } catch {
71
+ }
72
+ }
73
+ /**
74
+ * Loads session data from disk
75
+ * @returns SessionData if exists and valid, null otherwise
76
+ */
77
+ load() {
78
+ try {
79
+ if (!fs.existsSync(this.sessionFile)) {
80
+ return null;
81
+ }
82
+ const data = fs.readFileSync(this.sessionFile, "utf-8");
83
+ const session = JSON.parse(data);
84
+ if (typeof session.walletId !== "string" || typeof session.organizationId !== "string" || typeof session.authUserId !== "string" || typeof session.stamperKeys?.publicKey !== "string" || typeof session.stamperKeys?.secretKey !== "string" || typeof session.createdAt !== "number" || typeof session.updatedAt !== "number") {
85
+ return null;
86
+ }
87
+ return session;
88
+ } catch (error) {
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * Saves session data to disk with secure permissions (0o600)
94
+ * @param session Session data to save
95
+ */
96
+ save(session) {
97
+ this.ensureSessionDir();
98
+ const data = JSON.stringify(session, null, 2);
99
+ fs.writeFileSync(this.sessionFile, data, { mode: 384 });
100
+ }
101
+ /**
102
+ * Deletes the session file from disk
103
+ */
104
+ delete() {
105
+ try {
106
+ if (fs.existsSync(this.sessionFile)) {
107
+ fs.unlinkSync(this.sessionFile);
108
+ }
109
+ } catch (error) {
110
+ }
111
+ }
112
+ /**
113
+ * Checks if a session is expired
114
+ * @param session Session data to check
115
+ * @returns false - SSO sessions don't expire (stamper keys are permanent)
116
+ */
117
+ isExpired(_session) {
118
+ return false;
119
+ }
120
+ };
121
+
122
+ // src/auth/oauth.ts
123
+ var crypto = __toESM(require("crypto"));
124
+ var import_axios2 = __toESM(require("axios"));
125
+ var import_open = __toESM(require("open"));
126
+
127
+ // src/utils/logger.ts
128
+ var Logger = class {
129
+ constructor(context = "MCP") {
130
+ this.context = context;
131
+ }
132
+ /**
133
+ * Private method to write to stderr with proper formatting
134
+ */
135
+ log(level, message) {
136
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
137
+ const logMessage = `[${timestamp}] [${level}] [${this.context}] ${message}
138
+ `;
139
+ process.stderr.write(logMessage);
140
+ }
141
+ /**
142
+ * Log info message
143
+ */
144
+ info(message) {
145
+ this.log("INFO", message);
146
+ }
147
+ /**
148
+ * Log error message
149
+ */
150
+ error(message) {
151
+ this.log("ERROR", message);
152
+ }
153
+ /**
154
+ * Log warning message
155
+ */
156
+ warn(message) {
157
+ this.log("WARN", message);
158
+ }
159
+ /**
160
+ * Log debug message (only if DEBUG or PHANTOM_MCP_DEBUG env var is set)
161
+ */
162
+ debug(message) {
163
+ if (process.env.DEBUG || process.env.PHANTOM_MCP_DEBUG) {
164
+ this.log("DEBUG", message);
165
+ }
166
+ }
167
+ /**
168
+ * Create a child logger with combined context
169
+ * Example: parent context "MCP" + child "Transport" = "MCP:Transport"
170
+ */
171
+ child(childContext) {
172
+ return new Logger(`${this.context}:${childContext}`);
173
+ }
174
+ };
175
+ var logger = new Logger();
176
+
177
+ // src/auth/dcr.ts
178
+ var import_axios = __toESM(require("axios"));
179
+ var DCRClient = class {
180
+ /**
181
+ * Creates a new DCR client
182
+ *
183
+ * @param authBaseUrl - Base URL of the authorization server (default: https://auth.phantom.app or PHANTOM_AUTH_BASE_URL env var)
184
+ * @param appId - Application identifier prefix (default: phantom-mcp)
185
+ */
186
+ constructor(authBaseUrl = process.env.PHANTOM_AUTH_BASE_URL ?? "https://auth.phantom.app", appId = "phantom-mcp") {
187
+ this.authBaseUrl = authBaseUrl;
188
+ this.appId = appId;
189
+ this.logger = new Logger("DCR");
190
+ }
191
+ /**
192
+ * Registers a new OAuth client dynamically with the authorization server
193
+ *
194
+ * @param redirectUri - The redirect URI where the authorization server will send callbacks
195
+ * @returns Promise resolving to the client configuration (client_id, client_secret, etc.)
196
+ * @throws Error if registration fails
197
+ */
198
+ async register(redirectUri) {
199
+ const registrationEndpoint = `${this.authBaseUrl}/oauth/register`;
200
+ const clientName = `${this.appId}-${Date.now()}`;
201
+ const payload = {
202
+ client_name: clientName,
203
+ redirect_uris: [redirectUri],
204
+ grant_types: ["authorization_code", "refresh_token"],
205
+ response_types: ["code"],
206
+ application_type: "native",
207
+ token_endpoint_auth_method: "client_secret_basic"
208
+ };
209
+ this.logger.info(`Registering OAuth client: ${clientName}`);
210
+ this.logger.debug(`Registration endpoint: ${registrationEndpoint}`);
211
+ this.logger.debug(`Redirect URI: ${redirectUri}`);
212
+ try {
213
+ const response = await import_axios.default.post(registrationEndpoint, payload, {
214
+ headers: {
215
+ "Content-Type": "application/json"
216
+ }
217
+ });
218
+ this.logger.info(`Successfully registered client: ${response.data.client_id}`);
219
+ return {
220
+ client_id: response.data.client_id,
221
+ client_secret: response.data.client_secret,
222
+ client_id_issued_at: response.data.client_id_issued_at
223
+ };
224
+ } catch (error) {
225
+ const axiosError = error;
226
+ const errorMessage = axiosError.response?.data ? JSON.stringify(axiosError.response.data) : axiosError.message;
227
+ this.logger.error(`Failed to register OAuth client: ${errorMessage}`);
228
+ throw new Error(`Dynamic Client Registration failed: ${errorMessage}`);
229
+ }
230
+ }
231
+ };
232
+
233
+ // src/auth/callback-server.ts
234
+ var http = __toESM(require("http"));
235
+ var import_url = require("url");
236
+ var CallbackServer = class {
237
+ /**
238
+ * Creates a new callback server
239
+ *
240
+ * @param options - Server configuration options
241
+ * @param options.port - Port to listen on (default: 8080)
242
+ * @param options.host - Host to bind to (default: localhost)
243
+ * @param options.path - Callback path (default: /callback)
244
+ * @param options.timeoutMs - Timeout in milliseconds (default: 300000 = 5 minutes)
245
+ */
246
+ constructor(options = {}) {
247
+ this.server = null;
248
+ this.listeningPromise = null;
249
+ this.listeningResolve = null;
250
+ this.listeningReject = null;
251
+ this.port = options.port ?? 8080;
252
+ this.host = options.host ?? "localhost";
253
+ this.path = options.path ?? "/callback";
254
+ this.timeoutMs = options.timeoutMs ?? 3e5;
255
+ this.logger = new Logger("CallbackServer");
256
+ }
257
+ /**
258
+ * Gets the callback URL that should be used in OAuth authorization requests
259
+ *
260
+ * @returns The callback URL (e.g., http://localhost:8080/callback)
261
+ */
262
+ getCallbackUrl() {
263
+ return `http://${this.host}:${this.port}${this.path}`;
264
+ }
265
+ /**
266
+ * Starts the server and waits for an OAuth callback
267
+ *
268
+ * This method:
269
+ * 1. Starts an HTTP server on the configured host/port
270
+ * 2. Waits for a GET request to /callback
271
+ * 3. Validates the state parameter (CSRF protection)
272
+ * 4. Extracts OAuth parameters from the query string
273
+ * 5. Sends an HTML response to the browser
274
+ * 6. Closes the server
275
+ * 7. Returns the callback parameters
276
+ *
277
+ * @param expectedState - The expected state parameter value (for CSRF protection)
278
+ * @returns Promise resolving to the OAuth callback parameters
279
+ * @throws Error if the callback times out, state validation fails, or parameters are missing
280
+ */
281
+ async waitForCallback(expectedState) {
282
+ return new Promise((resolve, reject) => {
283
+ let timeoutId = null;
284
+ let hasResponded = false;
285
+ this.listeningPromise = new Promise((listeningResolve, listeningReject) => {
286
+ this.listeningResolve = listeningResolve;
287
+ this.listeningReject = listeningReject;
288
+ });
289
+ this.server = http.createServer((req, res) => {
290
+ if (req.url?.includes("favicon.ico")) {
291
+ res.writeHead(404);
292
+ res.end();
293
+ return;
294
+ }
295
+ if (req.method !== "GET" || !req.url) {
296
+ res.writeHead(404, { "Content-Type": "text/html" });
297
+ res.end(this.getErrorPage("Invalid endpoint"));
298
+ return;
299
+ }
300
+ let url;
301
+ try {
302
+ url = new import_url.URL(req.url, `http://${this.host}:${this.port}`);
303
+ } catch {
304
+ res.writeHead(404, { "Content-Type": "text/html" });
305
+ res.end(this.getErrorPage("Invalid endpoint"));
306
+ return;
307
+ }
308
+ if (url.pathname !== this.path) {
309
+ res.writeHead(404, { "Content-Type": "text/html" });
310
+ res.end(this.getErrorPage("Invalid endpoint"));
311
+ return;
312
+ }
313
+ if (hasResponded) {
314
+ return;
315
+ }
316
+ hasResponded = true;
317
+ try {
318
+ const response_type = url.searchParams.get("response_type");
319
+ const session_id = url.searchParams.get("session_id");
320
+ const wallet_id = url.searchParams.get("wallet_id");
321
+ const organization_id = url.searchParams.get("organization_id");
322
+ const auth_user_id = url.searchParams.get("auth_user_id");
323
+ this.logger.info("Received SSO callback");
324
+ this.logger.debug(`Session ID: ${session_id}`);
325
+ this.logger.debug(`Response type: ${response_type}`);
326
+ if (!session_id || session_id !== expectedState) {
327
+ const error = "Invalid session_id parameter";
328
+ this.logger.error(error);
329
+ res.writeHead(400, { "Content-Type": "text/html" });
330
+ res.end(this.getErrorPage("Authorization failed: Invalid session_id"));
331
+ this.cleanup(timeoutId);
332
+ reject(new Error(error));
333
+ return;
334
+ }
335
+ if (response_type !== "success") {
336
+ const error = `SSO flow failed with response_type: ${response_type}`;
337
+ this.logger.error(error);
338
+ res.writeHead(400, { "Content-Type": "text/html" });
339
+ res.end(this.getErrorPage(`Authorization failed: ${response_type}`));
340
+ this.cleanup(timeoutId);
341
+ reject(new Error(error));
342
+ return;
343
+ }
344
+ if (!wallet_id) {
345
+ const error = "Missing wallet_id parameter";
346
+ this.logger.error(error);
347
+ res.writeHead(400, { "Content-Type": "text/html" });
348
+ res.end(this.getErrorPage("Authorization failed: Missing wallet_id"));
349
+ this.cleanup(timeoutId);
350
+ reject(new Error(error));
351
+ return;
352
+ }
353
+ if (!organization_id) {
354
+ const error = "Missing organization_id parameter";
355
+ this.logger.error(error);
356
+ res.writeHead(400, { "Content-Type": "text/html" });
357
+ res.end(this.getErrorPage("Authorization failed: Missing organization_id"));
358
+ this.cleanup(timeoutId);
359
+ reject(new Error(error));
360
+ return;
361
+ }
362
+ if (!auth_user_id) {
363
+ const error = "Missing auth_user_id parameter";
364
+ this.logger.error(error);
365
+ res.writeHead(400, { "Content-Type": "text/html" });
366
+ res.end(this.getErrorPage("Authorization failed: Missing auth_user_id"));
367
+ this.cleanup(timeoutId);
368
+ reject(new Error(error));
369
+ return;
370
+ }
371
+ this.logger.info("SSO callback successful");
372
+ res.writeHead(200, { "Content-Type": "text/html" });
373
+ res.end(this.getSuccessPage());
374
+ this.cleanup(timeoutId);
375
+ resolve({
376
+ session_id,
377
+ wallet_id,
378
+ organization_id,
379
+ auth_user_id
380
+ });
381
+ } catch (error) {
382
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
383
+ this.logger.error(`Failed to process callback: ${errorMessage}`);
384
+ res.writeHead(500, { "Content-Type": "text/html" });
385
+ res.end(this.getErrorPage("Internal server error"));
386
+ this.cleanup(timeoutId);
387
+ reject(new Error(`Failed to process callback: ${errorMessage}`));
388
+ }
389
+ });
390
+ this.server.listen(this.port, this.host, () => {
391
+ this.logger.info(`Callback server listening on ${this.getCallbackUrl()}`);
392
+ this.listeningResolve?.();
393
+ this.listeningResolve = null;
394
+ this.listeningReject = null;
395
+ });
396
+ this.server.on("error", (error) => {
397
+ this.logger.error(`Server error: ${error.message}`);
398
+ this.listeningReject?.(error);
399
+ this.listeningResolve = null;
400
+ this.listeningReject = null;
401
+ this.cleanup(timeoutId);
402
+ reject(new Error(`Server error: ${error.message}`));
403
+ });
404
+ timeoutId = setTimeout(() => {
405
+ if (!hasResponded) {
406
+ hasResponded = true;
407
+ this.logger.error("Callback timeout");
408
+ this.cleanup(null);
409
+ reject(new Error("OAuth callback timeout"));
410
+ }
411
+ }, this.timeoutMs);
412
+ });
413
+ }
414
+ /**
415
+ * Cleans up the server and timeout
416
+ *
417
+ * @param timeoutId - The timeout ID to clear, or null
418
+ */
419
+ cleanup(timeoutId) {
420
+ if (timeoutId) {
421
+ clearTimeout(timeoutId);
422
+ }
423
+ if (this.server) {
424
+ this.server.close(() => {
425
+ this.logger.info("Callback server closed");
426
+ });
427
+ this.server = null;
428
+ }
429
+ this.listeningPromise = null;
430
+ this.listeningResolve = null;
431
+ this.listeningReject = null;
432
+ }
433
+ /**
434
+ * Waits until the callback server is listening for requests
435
+ *
436
+ * @returns Promise resolving when the server is listening
437
+ */
438
+ async waitForListening() {
439
+ if (this.server?.listening) {
440
+ return;
441
+ }
442
+ if (!this.listeningPromise) {
443
+ throw new Error("Callback server has not been started");
444
+ }
445
+ return this.listeningPromise;
446
+ }
447
+ /**
448
+ * Generates an HTML success page
449
+ *
450
+ * @returns HTML string
451
+ */
452
+ getSuccessPage() {
453
+ return `<!DOCTYPE html>
454
+ <html>
455
+ <head>
456
+ <meta charset="UTF-8">
457
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
458
+ <title>Authorization Successful</title>
459
+ <style>
460
+ body {
461
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
462
+ display: flex;
463
+ justify-content: center;
464
+ align-items: center;
465
+ min-height: 100vh;
466
+ margin: 0;
467
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
468
+ }
469
+ .container {
470
+ background: white;
471
+ padding: 3rem;
472
+ border-radius: 1rem;
473
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
474
+ text-align: center;
475
+ max-width: 500px;
476
+ }
477
+ .checkmark {
478
+ width: 80px;
479
+ height: 80px;
480
+ border-radius: 50%;
481
+ display: block;
482
+ stroke-width: 2;
483
+ stroke: #4CAF50;
484
+ stroke-miterlimit: 10;
485
+ margin: 0 auto 2rem;
486
+ box-shadow: inset 0px 0px 0px #4CAF50;
487
+ animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
488
+ }
489
+ .checkmark__circle {
490
+ stroke-dasharray: 166;
491
+ stroke-dashoffset: 166;
492
+ stroke-width: 2;
493
+ stroke-miterlimit: 10;
494
+ stroke: #4CAF50;
495
+ fill: none;
496
+ animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
497
+ }
498
+ .checkmark__check {
499
+ transform-origin: 50% 50%;
500
+ stroke-dasharray: 48;
501
+ stroke-dashoffset: 48;
502
+ animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
503
+ }
504
+ @keyframes stroke {
505
+ 100% {
506
+ stroke-dashoffset: 0;
507
+ }
508
+ }
509
+ @keyframes scale {
510
+ 0%, 100% {
511
+ transform: none;
512
+ }
513
+ 50% {
514
+ transform: scale3d(1.1, 1.1, 1);
515
+ }
516
+ }
517
+ @keyframes fill {
518
+ 100% {
519
+ box-shadow: inset 0px 0px 0px 30px #4CAF50;
520
+ }
521
+ }
522
+ h1 {
523
+ color: #333;
524
+ margin-bottom: 1rem;
525
+ }
526
+ p {
527
+ color: #666;
528
+ line-height: 1.6;
529
+ }
530
+ </style>
531
+ </head>
532
+ <body>
533
+ <div class="container">
534
+ <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
535
+ <circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none"/>
536
+ <path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
537
+ </svg>
538
+ <h1>Authorization Successful!</h1>
539
+ <p>You have successfully connected your Phantom wallet.</p>
540
+ <p>You can close this window and return to your application.</p>
541
+ </div>
542
+ </body>
543
+ </html>`;
544
+ }
545
+ /**
546
+ * Generates an HTML error page
547
+ *
548
+ * @param message - Error message to display
549
+ * @returns HTML string
550
+ */
551
+ getErrorPage(message) {
552
+ return `<!DOCTYPE html>
553
+ <html>
554
+ <head>
555
+ <meta charset="UTF-8">
556
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
557
+ <title>Authorization Failed</title>
558
+ <style>
559
+ body {
560
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
561
+ display: flex;
562
+ justify-content: center;
563
+ align-items: center;
564
+ min-height: 100vh;
565
+ margin: 0;
566
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
567
+ }
568
+ .container {
569
+ background: white;
570
+ padding: 3rem;
571
+ border-radius: 1rem;
572
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
573
+ text-align: center;
574
+ max-width: 500px;
575
+ }
576
+ .error-icon {
577
+ width: 80px;
578
+ height: 80px;
579
+ border-radius: 50%;
580
+ display: block;
581
+ stroke-width: 2;
582
+ stroke: #f44336;
583
+ stroke-miterlimit: 10;
584
+ margin: 0 auto 2rem;
585
+ box-shadow: inset 0px 0px 0px #f44336;
586
+ animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
587
+ }
588
+ .error-icon__circle {
589
+ stroke-dasharray: 166;
590
+ stroke-dashoffset: 166;
591
+ stroke-width: 2;
592
+ stroke-miterlimit: 10;
593
+ stroke: #f44336;
594
+ fill: none;
595
+ animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
596
+ }
597
+ .error-icon__cross {
598
+ transform-origin: 50% 50%;
599
+ stroke-dasharray: 48;
600
+ stroke-dashoffset: 48;
601
+ animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
602
+ }
603
+ @keyframes stroke {
604
+ 100% {
605
+ stroke-dashoffset: 0;
606
+ }
607
+ }
608
+ @keyframes scale {
609
+ 0%, 100% {
610
+ transform: none;
611
+ }
612
+ 50% {
613
+ transform: scale3d(1.1, 1.1, 1);
614
+ }
615
+ }
616
+ @keyframes fill {
617
+ 100% {
618
+ box-shadow: inset 0px 0px 0px 30px #f44336;
619
+ }
620
+ }
621
+ h1 {
622
+ color: #333;
623
+ margin-bottom: 1rem;
624
+ }
625
+ p {
626
+ color: #666;
627
+ line-height: 1.6;
628
+ }
629
+ .error-message {
630
+ color: #f44336;
631
+ font-weight: 500;
632
+ }
633
+ </style>
634
+ </head>
635
+ <body>
636
+ <div class="container">
637
+ <svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
638
+ <circle class="error-icon__circle" cx="26" cy="26" r="25" fill="none"/>
639
+ <path class="error-icon__cross" fill="none" d="M16 16 36 36 M36 16 16 36"/>
640
+ </svg>
641
+ <h1>Authorization Failed</h1>
642
+ <p class="error-message">${this.escapeHtml(message)}</p>
643
+ <p>Please close this window and try again.</p>
644
+ </div>
645
+ </body>
646
+ </html>`;
647
+ }
648
+ /**
649
+ * Escapes HTML special characters
650
+ *
651
+ * @param text - Text to escape
652
+ * @returns Escaped text
653
+ */
654
+ escapeHtml(text) {
655
+ const map = {
656
+ "&": "&amp;",
657
+ "<": "&lt;",
658
+ ">": "&gt;",
659
+ '"': "&quot;",
660
+ "'": "&#039;"
661
+ };
662
+ return text.replace(/[&<>"']/g, (char) => map[char]);
663
+ }
664
+ };
665
+
666
+ // src/auth/oauth.ts
667
+ var OAuthFlow = class {
668
+ /**
669
+ * Creates a new OAuth flow
670
+ *
671
+ * @param options - OAuth flow configuration
672
+ * @param options.authBaseUrl - Base URL of the authorization server (default: https://auth.phantom.app or PHANTOM_AUTH_BASE_URL env var)
673
+ * @param options.connectBaseUrl - Base URL of Phantom Connect (default: https://connect.phantom.app or PHANTOM_CONNECT_BASE_URL env var)
674
+ * @param options.callbackPort - Port for the local callback server (default: 8080 or PHANTOM_CALLBACK_PORT env var)
675
+ * @param options.callbackPath - Path for the OAuth callback (default: /callback or PHANTOM_CALLBACK_PATH env var)
676
+ * @param options.appId - Application identifier prefix (default: phantom-mcp)
677
+ * @param options.provider - SSO provider (default: google or PHANTOM_SSO_PROVIDER env var)
678
+ */
679
+ constructor(options = {}) {
680
+ this.authBaseUrl = options.authBaseUrl ?? process.env.PHANTOM_AUTH_BASE_URL ?? "https://auth.phantom.app";
681
+ this.connectBaseUrl = options.connectBaseUrl ?? process.env.PHANTOM_CONNECT_BASE_URL ?? "https://connect.phantom.app";
682
+ const envPort = process.env.PHANTOM_CALLBACK_PORT?.trim();
683
+ const defaultPort = 8080;
684
+ if (options.callbackPort !== void 0) {
685
+ const port = options.callbackPort;
686
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
687
+ throw new Error(`Invalid callbackPort: "${port}". Must be a valid port number between 1 and 65535.`);
688
+ }
689
+ this.callbackPort = port;
690
+ } else if (envPort !== void 0) {
691
+ const port = parseInt(envPort, 10);
692
+ if (isNaN(port) || port <= 0 || port > 65535) {
693
+ throw new Error(
694
+ `Invalid PHANTOM_CALLBACK_PORT: "${envPort}". Must be a valid port number between 1 and 65535.`
695
+ );
696
+ }
697
+ this.callbackPort = port;
698
+ } else {
699
+ this.callbackPort = defaultPort;
700
+ }
701
+ this.callbackPath = options.callbackPath ?? process.env.PHANTOM_CALLBACK_PATH ?? "/callback";
702
+ this.appId = options.appId ?? "phantom-mcp";
703
+ const provider = options.provider ?? process.env.PHANTOM_SSO_PROVIDER ?? "google";
704
+ if (!["google", "apple", "phantom"].includes(provider)) {
705
+ throw new Error(`Unsupported SSO provider: ${provider}`);
706
+ }
707
+ this.provider = provider;
708
+ this.logger = new Logger("OAuthFlow");
709
+ }
710
+ /**
711
+ * Executes the complete SSO authentication flow
712
+ *
713
+ * @returns Promise resolving to tokens, wallet/org IDs, client config, and stamper public key
714
+ * @throws Error if any step of the flow fails
715
+ */
716
+ async authenticate() {
717
+ this.logger.info("Starting SSO authentication flow");
718
+ const callbackServer = new CallbackServer({
719
+ port: this.callbackPort,
720
+ path: this.callbackPath
721
+ });
722
+ const redirectUri = callbackServer.getCallbackUrl();
723
+ let clientConfig;
724
+ const envClientId = (process.env.PHANTOM_APP_ID || process.env.PHANTOM_CLIENT_ID)?.trim();
725
+ const envClientSecret = process.env.PHANTOM_CLIENT_SECRET?.trim();
726
+ const hasClientId = envClientId && envClientId.length > 0;
727
+ const hasClientSecret = envClientSecret && envClientSecret.length > 0;
728
+ if (hasClientId) {
729
+ this.logger.info("Step 1: Using client credentials from environment variables");
730
+ const clientType = hasClientSecret ? "confidential" : "public";
731
+ this.logger.info(`Client type: ${clientType}`);
732
+ clientConfig = {
733
+ client_id: envClientId,
734
+ client_secret: envClientSecret || "",
735
+ // Empty string for public clients
736
+ client_id_issued_at: Math.floor(Date.now() / 1e3)
737
+ };
738
+ this.logger.info(`Using app ID: ${clientConfig.client_id}`);
739
+ } else {
740
+ this.logger.info("Step 1: Registering OAuth client via DCR");
741
+ this.logger.warn(
742
+ "DCR is not currently supported by auth.phantom.app - you should provide PHANTOM_APP_ID or PHANTOM_CLIENT_ID"
743
+ );
744
+ const dcrClient = new DCRClient(this.authBaseUrl, this.appId);
745
+ clientConfig = await dcrClient.register(redirectUri);
746
+ this.logger.info(`Client registered with ID: ${clientConfig.client_id}`);
747
+ }
748
+ this.logger.info("Step 2: Generating stamper keypair");
749
+ const { generateKeyPair } = await import("@phantom/crypto");
750
+ const stamperKeys = generateKeyPair();
751
+ this.logger.info(`Stamper public key: ${stamperKeys.publicKey}`);
752
+ this.logger.info("Step 3: Generating session ID");
753
+ const sessionId = this.generateSessionId();
754
+ this.logger.debug(`Session ID: ${sessionId}`);
755
+ this.logger.info("Step 4: Building SSO authorization URL");
756
+ const authUrl = this.buildAuthorizationUrl(clientConfig.client_id, redirectUri, stamperKeys.publicKey, sessionId);
757
+ this.logger.debug(`Authorization URL: ${authUrl}`);
758
+ this.logger.info("Step 5: Starting callback server");
759
+ const callbackPromise = callbackServer.waitForCallback(sessionId);
760
+ await callbackServer.waitForListening();
761
+ this.logger.info(`Step 6: Opening browser for ${this.provider} authentication`);
762
+ try {
763
+ await (0, import_open.default)(authUrl);
764
+ } catch (error) {
765
+ const errorMessage = error instanceof Error ? error.message : String(error);
766
+ this.logger.error(`Failed to automatically open browser for ${this.provider} authentication: ${errorMessage}`);
767
+ this.logger.error(`Auth URL: ${authUrl}`);
768
+ this.logger.info("Please open the following URL manually in your browser to complete authentication:");
769
+ this.logger.info(authUrl);
770
+ }
771
+ this.logger.info("Step 7: Waiting for SSO callback");
772
+ const callbackParams = await callbackPromise;
773
+ this.logger.info("Callback received successfully");
774
+ this.logger.debug(`Wallet ID: ${callbackParams.wallet_id}`);
775
+ this.logger.debug(`Organization ID: ${callbackParams.organization_id}`);
776
+ this.logger.debug(`Auth User ID: ${callbackParams.auth_user_id}`);
777
+ return {
778
+ walletId: callbackParams.wallet_id,
779
+ organizationId: callbackParams.organization_id,
780
+ authUserId: callbackParams.auth_user_id,
781
+ clientConfig,
782
+ stamperKeys
783
+ };
784
+ }
785
+ /**
786
+ * Refreshes an access token using a refresh token
787
+ *
788
+ * Note: Not used in SSO flow, kept for future OAuth compatibility.
789
+ *
790
+ * @param refreshToken - The refresh token
791
+ * @param clientConfig - The OAuth client configuration
792
+ * @returns Promise resolving to new tokens
793
+ * @throws Error if token refresh fails
794
+ */
795
+ async refreshToken(refreshToken, clientConfig) {
796
+ this.logger.info("Refreshing access token");
797
+ const tokenEndpoint = `${this.authBaseUrl}/oauth2/token`;
798
+ const isPublicClient = !clientConfig.client_secret || clientConfig.client_secret.length === 0;
799
+ const params = {
800
+ grant_type: "refresh_token",
801
+ refresh_token: refreshToken
802
+ };
803
+ if (isPublicClient) {
804
+ params.client_id = clientConfig.client_id;
805
+ }
806
+ const headers = {
807
+ "Content-Type": "application/x-www-form-urlencoded"
808
+ };
809
+ if (!isPublicClient) {
810
+ const basicAuth = Buffer.from(`${clientConfig.client_id}:${clientConfig.client_secret}`).toString("base64");
811
+ headers.Authorization = `Basic ${basicAuth}`;
812
+ }
813
+ try {
814
+ const response = await import_axios2.default.post(tokenEndpoint, new URLSearchParams(params).toString(), {
815
+ headers,
816
+ timeout: 3e4
817
+ });
818
+ this.logger.info("Token refresh successful");
819
+ return {
820
+ access_token: response.data.access_token,
821
+ refresh_token: response.data.refresh_token,
822
+ expires_in: response.data.expires_in
823
+ };
824
+ } catch (error) {
825
+ const axiosError = error;
826
+ const errorMessage = axiosError.response?.data ? JSON.stringify(axiosError.response.data) : axiosError.message;
827
+ this.logger.error(`Token refresh failed: ${errorMessage}`);
828
+ throw new Error(`Token refresh failed: ${errorMessage}`);
829
+ }
830
+ }
831
+ /**
832
+ * Generates a random session ID for SSO flow
833
+ *
834
+ * @returns Random session ID string
835
+ */
836
+ generateSessionId() {
837
+ return this.base64URLEncode(crypto.randomBytes(32));
838
+ }
839
+ /**
840
+ * Encodes a buffer to base64url format (RFC 4648)
841
+ * Base64url encoding is base64 with URL-safe characters:
842
+ * - Replace + with -
843
+ * - Replace / with _
844
+ * - Remove padding =
845
+ *
846
+ * @param buffer - Buffer to encode
847
+ * @returns Base64url encoded string
848
+ */
849
+ base64URLEncode(buffer) {
850
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
851
+ }
852
+ /**
853
+ * Builds the SSO authorization URL
854
+ *
855
+ * @param appId - Application ID
856
+ * @param redirectUri - Callback redirect URI
857
+ * @param publicKey - Stamper public key
858
+ * @param sessionId - Session ID for correlation
859
+ * @returns Authorization URL
860
+ */
861
+ buildAuthorizationUrl(appId, redirectUri, publicKey, sessionId) {
862
+ const params = new URLSearchParams({
863
+ provider: this.provider,
864
+ app_id: appId,
865
+ redirect_uri: redirectUri,
866
+ public_key: publicKey,
867
+ session_id: sessionId,
868
+ sdk_version: "1.0.0",
869
+ sdk_type: "mcp-server"
870
+ });
871
+ return `${this.connectBaseUrl}/login?${params.toString()}`;
872
+ }
873
+ /**
874
+ * Exchanges an authorization code for access and refresh tokens
875
+ *
876
+ * Note: Not used in SSO flow, kept for future OAuth compatibility.
877
+ *
878
+ * @param code - Authorization code
879
+ * @param redirectUri - Callback redirect URI (must match the one used in authorization)
880
+ * @param clientConfig - OAuth client configuration
881
+ * @returns Promise resolving to tokens
882
+ * @throws Error if token exchange fails
883
+ */
884
+ // @ts-expect-error - Unused in SSO flow, kept for future OAuth support
885
+ async exchangeCodeForTokens(code, redirectUri, clientConfig) {
886
+ const tokenEndpoint = `${this.authBaseUrl}/oauth2/token`;
887
+ const isPublicClient = !clientConfig.client_secret || clientConfig.client_secret.length === 0;
888
+ const params = {
889
+ grant_type: "authorization_code",
890
+ code,
891
+ redirect_uri: redirectUri
892
+ };
893
+ if (isPublicClient) {
894
+ params.client_id = clientConfig.client_id;
895
+ }
896
+ const headers = {
897
+ "Content-Type": "application/x-www-form-urlencoded"
898
+ };
899
+ if (!isPublicClient) {
900
+ const basicAuth = Buffer.from(`${clientConfig.client_id}:${clientConfig.client_secret}`).toString("base64");
901
+ headers.Authorization = `Basic ${basicAuth}`;
902
+ }
903
+ try {
904
+ const response = await import_axios2.default.post(tokenEndpoint, new URLSearchParams(params).toString(), {
905
+ headers,
906
+ timeout: 3e4
907
+ });
908
+ return {
909
+ access_token: response.data.access_token,
910
+ refresh_token: response.data.refresh_token,
911
+ expires_in: response.data.expires_in
912
+ };
913
+ } catch (error) {
914
+ const axiosError = error;
915
+ const errorMessage = axiosError.response?.data ? JSON.stringify(axiosError.response.data) : axiosError.message;
916
+ this.logger.error(`Token exchange failed: ${errorMessage}`);
917
+ throw new Error(`Token exchange failed: ${errorMessage}`);
918
+ }
919
+ }
920
+ };
921
+
922
+ // src/session/manager.ts
923
+ var SessionManager = class {
924
+ /**
925
+ * Creates a new SessionManager
926
+ *
927
+ * @param options - Configuration options
928
+ */
929
+ constructor(options = {}) {
930
+ this.session = null;
931
+ this.client = null;
932
+ this.logger = new Logger("SessionManager");
933
+ this.authBaseUrl = options.authBaseUrl ?? process.env.PHANTOM_AUTH_BASE_URL ?? "https://auth.phantom.app";
934
+ this.connectBaseUrl = options.connectBaseUrl;
935
+ this.apiBaseUrl = options.apiBaseUrl ?? process.env.PHANTOM_API_BASE_URL ?? "https://api.phantom.app/v1/wallets";
936
+ const defaultPort = 8080;
937
+ const parseEnvPort = (value) => {
938
+ const parsed = Number.parseInt(value, 10);
939
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
940
+ return null;
941
+ }
942
+ return parsed;
943
+ };
944
+ if (options.callbackPort !== void 0) {
945
+ if (!Number.isInteger(options.callbackPort) || options.callbackPort <= 0 || options.callbackPort > 65535) {
946
+ throw new Error(
947
+ `Invalid callbackPort: "${options.callbackPort}". Must be a valid port number between 1 and 65535.`
948
+ );
949
+ }
950
+ this.callbackPort = options.callbackPort;
951
+ } else {
952
+ const envPort = process.env.PHANTOM_CALLBACK_PORT?.trim();
953
+ const parsedEnvPort = envPort ? parseEnvPort(envPort) : null;
954
+ if (envPort && parsedEnvPort === null) {
955
+ this.logger.warn(`Invalid PHANTOM_CALLBACK_PORT "${envPort}". Falling back to ${defaultPort}.`);
956
+ this.callbackPort = defaultPort;
957
+ } else {
958
+ this.callbackPort = parsedEnvPort ?? defaultPort;
959
+ }
960
+ }
961
+ this.callbackPath = options.callbackPath ?? process.env.PHANTOM_CALLBACK_PATH ?? "/callback";
962
+ this.appId = options.appId ?? "phantom-mcp";
963
+ this.storage = new SessionStorage(options.sessionDir);
964
+ }
965
+ /**
966
+ * Initializes the session manager
967
+ * Loads existing session or authenticates if needed
968
+ *
969
+ * @throws Error if authentication fails
970
+ */
971
+ async initialize() {
972
+ this.logger.info("Initializing session manager");
973
+ const existingSession = this.storage.load();
974
+ if (existingSession && !this.storage.isExpired(existingSession)) {
975
+ this.logger.info("Loaded valid session from storage");
976
+ this.session = existingSession;
977
+ this.createClient();
978
+ return;
979
+ }
980
+ if (existingSession) {
981
+ this.logger.info("Session expired, re-authenticating");
982
+ } else {
983
+ this.logger.info("No session found, authenticating");
984
+ }
985
+ await this.authenticate();
986
+ }
987
+ /**
988
+ * Returns the initialized PhantomClient
989
+ *
990
+ * @returns PhantomClient instance
991
+ * @throws Error if not initialized
992
+ */
993
+ getClient() {
994
+ if (!this.client) {
995
+ throw new Error("SessionManager not initialized. Call initialize() first.");
996
+ }
997
+ return this.client;
998
+ }
999
+ /**
1000
+ * Returns the current session data
1001
+ *
1002
+ * @returns Current session data
1003
+ * @throws Error if not initialized
1004
+ */
1005
+ getSession() {
1006
+ if (!this.session) {
1007
+ throw new Error("SessionManager not initialized. Call initialize() first.");
1008
+ }
1009
+ return this.session;
1010
+ }
1011
+ /**
1012
+ * Resets the session by clearing stored data and re-authenticating
1013
+ *
1014
+ * @throws Error if authentication fails
1015
+ */
1016
+ async resetSession() {
1017
+ this.logger.info("Resetting session");
1018
+ this.storage.delete();
1019
+ this.session = null;
1020
+ this.client = null;
1021
+ await this.authenticate();
1022
+ }
1023
+ /**
1024
+ * Executes the SSO flow and creates a new session
1025
+ * Steps:
1026
+ * 1. Execute SSO flow to get wallet/org IDs and stamper keypair
1027
+ * 2. Create SessionData with SSO result and stamper keys
1028
+ * 3. Save to storage
1029
+ * 4. Create PhantomClient
1030
+ *
1031
+ * Note: Stamper keypair is generated during SSO flow and public key is sent to auth server
1032
+ *
1033
+ * @throws Error if SSO flow fails
1034
+ */
1035
+ async authenticate() {
1036
+ this.logger.info("Starting authentication");
1037
+ const oauthFlow = new OAuthFlow({
1038
+ authBaseUrl: this.authBaseUrl,
1039
+ connectBaseUrl: this.connectBaseUrl,
1040
+ callbackPort: this.callbackPort,
1041
+ callbackPath: this.callbackPath,
1042
+ appId: this.appId
1043
+ });
1044
+ const oauthResult = await oauthFlow.authenticate();
1045
+ this.logger.info("SSO flow completed successfully");
1046
+ const now = Math.floor(Date.now() / 1e3);
1047
+ this.session = {
1048
+ walletId: oauthResult.walletId,
1049
+ organizationId: oauthResult.organizationId,
1050
+ authUserId: oauthResult.authUserId,
1051
+ stamperKeys: oauthResult.stamperKeys,
1052
+ createdAt: now,
1053
+ updatedAt: now
1054
+ };
1055
+ this.storage.save(this.session);
1056
+ this.logger.info("Session saved to storage");
1057
+ this.createClient();
1058
+ }
1059
+ /**
1060
+ * Creates a PhantomClient instance from the current session
1061
+ * Steps:
1062
+ * 1. Create ApiKeyStamper with session keypair
1063
+ * 2. Create PhantomClient with stamper, organizationId, and app headers
1064
+ * 3. Set walletType to 'user-wallet'
1065
+ *
1066
+ * @throws Error if session is not available
1067
+ */
1068
+ createClient() {
1069
+ if (!this.session) {
1070
+ throw new Error("Cannot create client without session");
1071
+ }
1072
+ this.logger.info("Creating PhantomClient");
1073
+ const stamper = new import_api_key_stamper.ApiKeyStamper({
1074
+ apiSecretKey: this.session.stamperKeys.secretKey
1075
+ });
1076
+ const appId = process.env.PHANTOM_APP_ID || process.env.PHANTOM_CLIENT_ID || this.appId;
1077
+ this.client = new import_client.PhantomClient(
1078
+ {
1079
+ apiBaseUrl: this.apiBaseUrl,
1080
+ organizationId: this.session.organizationId,
1081
+ walletType: "user-wallet",
1082
+ headers: {
1083
+ "X-App-Id": appId
1084
+ }
1085
+ // Type assertion needed as X-App-Id is not in SdkAnalyticsHeaders
1086
+ },
1087
+ stamper
1088
+ );
1089
+ this.logger.info("PhantomClient created successfully");
1090
+ }
1091
+ };
1092
+
1093
+ // src/tools/get-wallet-addresses.ts
1094
+ var getWalletAddressesTool = {
1095
+ name: "get_wallet_addresses",
1096
+ description: "Gets all blockchain addresses for the authenticated embedded wallet (Solana, Ethereum, Bitcoin, Sui)",
1097
+ inputSchema: {
1098
+ type: "object",
1099
+ properties: {
1100
+ derivationIndex: {
1101
+ type: "number",
1102
+ description: "Optional derivation index for the addresses",
1103
+ minimum: 0
1104
+ }
1105
+ }
1106
+ },
1107
+ handler: async (params, context) => {
1108
+ const { client, session, logger: logger2 } = context;
1109
+ const derivationIndex = typeof params.derivationIndex === "number" ? params.derivationIndex : void 0;
1110
+ logger2.info("Getting addresses for wallet");
1111
+ try {
1112
+ const addresses = await client.getWalletAddresses(
1113
+ session.walletId,
1114
+ void 0,
1115
+ // Use default derivation paths (Solana, Ethereum, Bitcoin, Sui)
1116
+ derivationIndex
1117
+ );
1118
+ logger2.info(`Successfully retrieved ${addresses.length} addresses`);
1119
+ return {
1120
+ walletId: session.walletId,
1121
+ organizationId: session.organizationId,
1122
+ addresses: addresses.map((addr) => ({
1123
+ addressType: addr.addressType,
1124
+ address: addr.address
1125
+ }))
1126
+ };
1127
+ } catch (error) {
1128
+ const errorMessage = error instanceof Error ? error.message : String(error);
1129
+ logger2.error(`Failed to get wallet addresses: ${errorMessage}`);
1130
+ throw new Error(`Failed to get wallet addresses: ${errorMessage}`);
1131
+ }
1132
+ }
1133
+ };
1134
+
1135
+ // src/tools/sign-transaction.ts
1136
+ var signTransactionTool = {
1137
+ name: "sign_transaction",
1138
+ description: "Signs a transaction using the authenticated embedded wallet. Supports Solana, Ethereum, Bitcoin, and other chains.",
1139
+ inputSchema: {
1140
+ type: "object",
1141
+ properties: {
1142
+ walletId: {
1143
+ type: "string",
1144
+ description: "Optional wallet ID to use for signing (defaults to authenticated wallet)"
1145
+ },
1146
+ transaction: {
1147
+ type: "string",
1148
+ description: "The transaction to sign (format depends on chain: base64url for Solana, RLP-encoded hex for Ethereum)"
1149
+ },
1150
+ networkId: {
1151
+ type: "string",
1152
+ description: 'Network identifier (e.g., "eip155:1" for Ethereum mainnet, "solana:mainnet" for Solana)'
1153
+ },
1154
+ derivationIndex: {
1155
+ type: "number",
1156
+ description: "Optional derivation index for the account (default: 0)",
1157
+ minimum: 0
1158
+ },
1159
+ account: {
1160
+ type: "string",
1161
+ description: "Optional specific account address to use for simulation/signing"
1162
+ }
1163
+ },
1164
+ required: ["transaction", "networkId"]
1165
+ },
1166
+ handler: async (params, context) => {
1167
+ const { client, session, logger: logger2 } = context;
1168
+ if (typeof params.transaction !== "string") {
1169
+ throw new Error("transaction must be a string");
1170
+ }
1171
+ if (typeof params.networkId !== "string") {
1172
+ throw new Error("networkId must be a string");
1173
+ }
1174
+ const walletId = typeof params.walletId === "string" ? params.walletId : session.walletId;
1175
+ if (!walletId) {
1176
+ throw new Error("walletId is required (missing from session and not provided)");
1177
+ }
1178
+ if (params.derivationIndex !== void 0 && params.derivationIndex !== null) {
1179
+ const derivIdx = params.derivationIndex;
1180
+ if (!Number.isInteger(derivIdx) || derivIdx < 0) {
1181
+ throw new Error("derivationIndex must be a non-negative integer");
1182
+ }
1183
+ }
1184
+ if (params.account !== void 0 && typeof params.account !== "string") {
1185
+ throw new Error("account must be a string");
1186
+ }
1187
+ const transaction = params.transaction;
1188
+ const networkId = params.networkId;
1189
+ const derivationIndex = typeof params.derivationIndex === "number" ? params.derivationIndex : void 0;
1190
+ const account = typeof params.account === "string" ? params.account : void 0;
1191
+ logger2.info(`Signing transaction for wallet ${walletId} on network ${networkId}`);
1192
+ try {
1193
+ const result = await client.signTransaction({
1194
+ walletId,
1195
+ transaction,
1196
+ networkId,
1197
+ derivationIndex,
1198
+ account
1199
+ });
1200
+ logger2.info(`Successfully signed transaction for wallet ${walletId}`);
1201
+ return {
1202
+ signedTransaction: result.rawTransaction
1203
+ };
1204
+ } catch (error) {
1205
+ const errorMessage = error instanceof Error ? error.message : String(error);
1206
+ logger2.error(`Failed to sign transaction: ${errorMessage}`);
1207
+ throw new Error(`Failed to sign transaction: ${errorMessage}`);
1208
+ }
1209
+ }
1210
+ };
1211
+
1212
+ // src/tools/sign-message.ts
1213
+ var import_utils = require("@phantom/utils");
1214
+ var import_base64url = require("@phantom/base64url");
1215
+ var signMessageTool = {
1216
+ name: "sign_message",
1217
+ description: "Signs a UTF-8 message using the authenticated embedded wallet. Automatically routes to the correct signing method based on the network (Ethereum vs other chains).",
1218
+ inputSchema: {
1219
+ type: "object",
1220
+ properties: {
1221
+ walletId: {
1222
+ type: "string",
1223
+ description: "Optional wallet ID to use for signing (defaults to authenticated wallet)"
1224
+ },
1225
+ message: {
1226
+ type: "string",
1227
+ description: "The UTF-8 message to sign"
1228
+ },
1229
+ networkId: {
1230
+ type: "string",
1231
+ description: 'Network identifier (e.g., "eip155:1" for Ethereum mainnet, "solana:mainnet" for Solana)'
1232
+ },
1233
+ derivationIndex: {
1234
+ type: "integer",
1235
+ description: "Optional derivation index for the account (default: 0)",
1236
+ minimum: 0
1237
+ }
1238
+ },
1239
+ required: ["message", "networkId"]
1240
+ },
1241
+ handler: async (params, context) => {
1242
+ const { client, session, logger: logger2 } = context;
1243
+ if (typeof params.message !== "string") {
1244
+ throw new Error("message must be a string");
1245
+ }
1246
+ if (typeof params.networkId !== "string") {
1247
+ throw new Error("networkId must be a string");
1248
+ }
1249
+ const walletId = typeof params.walletId === "string" ? params.walletId : session.walletId;
1250
+ if (!walletId) {
1251
+ throw new Error("walletId is required (missing from session and not provided)");
1252
+ }
1253
+ if (params.derivationIndex !== void 0 && params.derivationIndex !== null) {
1254
+ const derivIdx = params.derivationIndex;
1255
+ if (!Number.isInteger(derivIdx) || derivIdx < 0) {
1256
+ throw new Error("derivationIndex must be a non-negative integer");
1257
+ }
1258
+ }
1259
+ const message = params.message;
1260
+ const networkId = params.networkId;
1261
+ const derivationIndex = typeof params.derivationIndex === "number" ? params.derivationIndex : void 0;
1262
+ logger2.info(`Signing message for wallet ${walletId} on network ${networkId}`);
1263
+ try {
1264
+ let signature;
1265
+ if ((0, import_utils.isEthereumChain)(networkId)) {
1266
+ const base64Message = (0, import_base64url.stringToBase64url)(message);
1267
+ logger2.debug("Using Ethereum message signing");
1268
+ signature = await client.ethereumSignMessage({
1269
+ walletId,
1270
+ message: base64Message,
1271
+ networkId,
1272
+ derivationIndex
1273
+ });
1274
+ } else {
1275
+ logger2.debug("Using UTF-8 message signing");
1276
+ signature = await client.signUtf8Message({
1277
+ walletId,
1278
+ message,
1279
+ networkId,
1280
+ derivationIndex
1281
+ });
1282
+ }
1283
+ logger2.info(`Successfully signed message for wallet ${walletId}`);
1284
+ return {
1285
+ signature
1286
+ };
1287
+ } catch (error) {
1288
+ const errorMessage = error instanceof Error ? error.message : String(error);
1289
+ logger2.error(`Failed to sign message: ${errorMessage}`);
1290
+ throw new Error(`Failed to sign message: ${errorMessage}`);
1291
+ }
1292
+ }
1293
+ };
1294
+
1295
+ // src/tools/index.ts
1296
+ var tools = [getWalletAddressesTool, signTransactionTool, signMessageTool];
1297
+ function getTool(name) {
1298
+ return tools.find((tool) => tool.name === name);
1299
+ }
1300
+
1301
+ // src/server.ts
1302
+ var PhantomMCPServer = class {
1303
+ /**
1304
+ * Creates a new PhantomMCPServer instance
1305
+ *
1306
+ * @param options - Configuration options
1307
+ */
1308
+ constructor(options = {}) {
1309
+ this.logger = new Logger("PhantomMCPServer");
1310
+ this.server = new import_server.Server(
1311
+ {
1312
+ name: "phantom-mcp-server",
1313
+ version: "1.0.0"
1314
+ },
1315
+ {
1316
+ capabilities: {
1317
+ tools: {}
1318
+ }
1319
+ }
1320
+ );
1321
+ this.sessionManager = new SessionManager(options.session);
1322
+ this.setupHandlers();
1323
+ this.logger.info("PhantomMCPServer initialized");
1324
+ }
1325
+ /**
1326
+ * Sets up MCP request handlers
1327
+ */
1328
+ setupHandlers() {
1329
+ this.server.setRequestHandler(import_types.ListToolsRequestSchema, () => {
1330
+ this.logger.info("Handling tools/list request");
1331
+ try {
1332
+ const toolDefinitions = tools.map((tool) => ({
1333
+ name: tool.name,
1334
+ description: tool.description,
1335
+ inputSchema: tool.inputSchema
1336
+ }));
1337
+ this.logger.info(`Returning ${toolDefinitions.length} tool definitions`);
1338
+ return {
1339
+ tools: toolDefinitions
1340
+ };
1341
+ } catch (error) {
1342
+ const errorMessage = error instanceof Error ? error.message : String(error);
1343
+ this.logger.error(`Failed to list tools: ${errorMessage}`);
1344
+ throw error;
1345
+ }
1346
+ });
1347
+ this.server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
1348
+ const toolName = request.params.name;
1349
+ this.logger.info(`Handling tools/call request for: ${toolName}`);
1350
+ try {
1351
+ const tool = getTool(toolName);
1352
+ if (!tool) {
1353
+ const error = `Unknown tool: ${toolName}`;
1354
+ this.logger.error(error);
1355
+ return {
1356
+ content: [
1357
+ {
1358
+ type: "text",
1359
+ text: JSON.stringify({ error }, null, 2)
1360
+ }
1361
+ ],
1362
+ isError: true
1363
+ };
1364
+ }
1365
+ const client = this.sessionManager.getClient();
1366
+ const session = this.sessionManager.getSession();
1367
+ const context = {
1368
+ client,
1369
+ session,
1370
+ logger: this.logger.child(toolName)
1371
+ };
1372
+ this.logger.info(`Executing tool: ${toolName}`);
1373
+ const result = await tool.handler(request.params.arguments ?? {}, context);
1374
+ this.logger.info(`Tool execution successful: ${toolName}`);
1375
+ return {
1376
+ content: [
1377
+ {
1378
+ type: "text",
1379
+ text: JSON.stringify(result, null, 2)
1380
+ }
1381
+ ]
1382
+ };
1383
+ } catch (error) {
1384
+ const errorMessage = error instanceof Error ? error.message : String(error);
1385
+ this.logger.error(`Tool execution failed for ${toolName}: ${errorMessage}`);
1386
+ if (error instanceof Error && error.stack) {
1387
+ this.logger.debug(`Stack trace: ${error.stack}`);
1388
+ }
1389
+ return {
1390
+ content: [
1391
+ {
1392
+ type: "text",
1393
+ text: JSON.stringify(
1394
+ {
1395
+ error: errorMessage
1396
+ },
1397
+ null,
1398
+ 2
1399
+ )
1400
+ }
1401
+ ],
1402
+ isError: true
1403
+ };
1404
+ }
1405
+ });
1406
+ this.logger.info("Request handlers registered");
1407
+ }
1408
+ /**
1409
+ * Starts the MCP server
1410
+ * - Initializes session (loads or authenticates)
1411
+ * - Connects stdio transport
1412
+ * - Begins listening for requests
1413
+ *
1414
+ * @throws Error if initialization or startup fails
1415
+ */
1416
+ async start() {
1417
+ this.logger.info("Starting PhantomMCPServer");
1418
+ try {
1419
+ this.logger.info("Initializing session");
1420
+ await this.sessionManager.initialize();
1421
+ this.logger.info("Session initialized successfully");
1422
+ this.logger.info("Connecting stdio transport");
1423
+ const transport = new import_stdio.StdioServerTransport();
1424
+ await this.server.connect(transport);
1425
+ this.logger.info("Server connected and ready to accept requests");
1426
+ } catch (error) {
1427
+ const errorMessage = error instanceof Error ? error.message : String(error);
1428
+ this.logger.error(`Failed to start server: ${errorMessage}`);
1429
+ throw error;
1430
+ }
1431
+ }
1432
+ };
1433
+
1434
+ // src/index.ts
1435
+ async function main() {
1436
+ const server = new PhantomMCPServer();
1437
+ await server.start();
1438
+ }
1439
+ if (require.main === module) {
1440
+ main().catch((error) => {
1441
+ console.error("Fatal error:", error);
1442
+ process.exit(1);
1443
+ });
1444
+ }
1445
+ // Annotate the CommonJS export names for ESM import in node:
1446
+ 0 && (module.exports = {
1447
+ SessionManager
1448
+ });