Skip to content

Commit 31dd6bd

Browse files
chore: sync from internal
1 parent 3a8c89b commit 31dd6bd

9 files changed

Lines changed: 106 additions & 44 deletions

File tree

.changeset/pre.json

Lines changed: 0 additions & 39 deletions
This file was deleted.

packages/mcp-server/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@phantom/mcp-server",
3-
"version": "1.0.0-beta.0",
3+
"version": "1.0.0",
44
"description": "MCP Server for Phantom Wallet",
55
"repository": {
66
"type": "git",
@@ -51,7 +51,6 @@
5151
"@phantom/perps-client": "workspace:^",
5252
"@phantom/phantom-api-client": "workspace:^",
5353
"@phantom/sdk-types": "workspace:^",
54-
"@phantom/server-sdk": "workspace:^",
5554
"@phantom/utils": "workspace:^",
5655
"@solana/spl-token": "^0.4.14",
5756
"@solana/web3.js": "^1.95.5",

packages/mcp-server/src/server.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ const mockSessionManagerInstance = {
3030
resetSession: jest.fn().mockResolvedValue(undefined),
3131
getClient: jest.fn(),
3232
getSession: jest.fn(),
33+
getOAuthHeaders: jest.fn().mockReturnValue({}),
3334
};
3435

3536
const mockSetHeaders = jest.fn();
37+
const mockSetGetHeaders = jest.fn();
3638
const mockSetPaymentHandler = jest.fn();
3739

3840
jest.mock("@modelcontextprotocol/sdk/server/index.js", () => ({
@@ -61,6 +63,7 @@ jest.mock("@phantom/phantom-api-client", () => {
6163
...actual,
6264
PhantomApiClient: jest.fn().mockImplementation(() => ({
6365
setHeaders: mockSetHeaders,
66+
setGetHeaders: mockSetGetHeaders,
6467
setPaymentHandler: mockSetPaymentHandler,
6568
})),
6669
};
@@ -75,6 +78,7 @@ beforeEach(() => {
7578
mockConnect.mockClear();
7679
mockSetRequestHandler.mockClear();
7780
mockSetHeaders.mockClear();
81+
mockSetGetHeaders.mockClear();
7882
mockSetPaymentHandler.mockClear();
7983
mockGetTool.mockReset();
8084
for (const tool of mockTools) {

packages/mcp-server/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ export class PhantomMCPServer {
473473
staticHeaders["X-Wallet-Address"] = solanaAddress;
474474
}
475475
this.apiClient.setHeaders(staticHeaders);
476+
this.apiClient.setGetHeaders(() => this.sessionManager.getOAuthHeaders());
476477

477478
this.apiClient.setPaymentHandler(async payment => {
478479
this.logger.info(`Paying ${payment.amount} ${payment.token} to unlock API access`);

packages/mcp-server/src/session/manager.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class SessionManager {
6868

6969
private session: SessionData | null = null;
7070
private client: PhantomClient | null = null;
71+
private stamper: InstanceType<typeof Auth2Stamper> | null = null;
7172

7273
private createMcpAnalyticsHeaders(appId: string): ServerSdkHeaders {
7374
return {
@@ -256,6 +257,7 @@ export class SessionManager {
256257
await this.clearDeviceCodeAuthState();
257258
this.session = null;
258259
this.client = null;
260+
this.stamper = null;
259261

260262
// Re-authenticate
261263
await this.authenticate();
@@ -427,6 +429,7 @@ export class SessionManager {
427429
redirectUri: "",
428430
});
429431
await stamper.init();
432+
this.stamper = stamper;
430433
if (!stamper.bearerToken || !stamper.auth2Token) {
431434
this.logger.warn(
432435
"device-code auth2 stamper state is missing tokens — deleting stale session and re-authenticating",
@@ -455,6 +458,19 @@ export class SessionManager {
455458
);
456459
}
457460

461+
/**
462+
* Returns dynamic OAuth headers for the current device-code session.
463+
* Called on every request so the bearer token is always fresh (handles refresh).
464+
* Returns an empty object for SSO sessions or when no stamper is available.
465+
*/
466+
getOAuthHeaders(): Record<string, string | undefined> {
467+
if (this.session?.authFlow !== "device-code" || !this.stamper) return {};
468+
return {
469+
authorization: this.stamper.bearerToken ?? undefined,
470+
"x-auth-user-id": this.stamper.auth2Token?.sub,
471+
};
472+
}
473+
458474
private async clearDeviceCodeAuthState(): Promise<void> {
459475
try {
460476
await new NodeFileAuth2StamperStorage(this.storage.sessionDir).clear();

packages/phantom-api-client/src/PhantomApiClient.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class PhantomApiClient {
4343
private staticHeaders: Record<string, string> = {};
4444
private paymentSignature: string | null = null;
4545
private paymentHandler: PaymentHandler | null = null;
46+
private getDynamicHeadersFn: (() => Record<string, string | undefined>) | null = null;
4647

4748
constructor(options: PhantomApiClientOptions) {
4849
this.baseUrl = options.baseUrl.replace(/\/$/, "");
@@ -67,6 +68,15 @@ export class PhantomApiClient {
6768
this.staticHeaders = { ...this.staticHeaders, ...headers };
6869
}
6970

71+
/**
72+
* Set a callback invoked on every request to supply dynamic headers (e.g. a
73+
* refreshable OAuth bearer token). Dynamic headers are merged after static
74+
* headers so they can override them. Undefined values are omitted.
75+
*/
76+
setGetHeaders(fn: () => Record<string, string | undefined>): void {
77+
this.getDynamicHeadersFn = fn;
78+
}
79+
7080
/** Stored after a successful payment — sent as X-Payment on all subsequent requests */
7181
setPaymentSignature(sig: string): void {
7282
this.paymentSignature = sig;
@@ -147,8 +157,15 @@ export class PhantomApiClient {
147157
const headers: Record<string, string> = {
148158
"Content-Type": "application/json",
149159
...this.staticHeaders,
150-
...extra,
151160
};
161+
if (this.getDynamicHeadersFn) {
162+
for (const [k, v] of Object.entries(this.getDynamicHeadersFn())) {
163+
if (v !== undefined) headers[k] = v;
164+
}
165+
}
166+
if (extra) {
167+
Object.assign(headers, extra);
168+
}
152169
if (this.paymentSignature) {
153170
headers["X-Payment"] = this.paymentSignature;
154171
}

packages/phantom-api-client/src/__tests__/PhantomApiClient.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,71 @@ describe("PhantomApiClient", () => {
136136
});
137137
});
138138

139+
describe("setGetHeaders()", () => {
140+
it("includes dynamic header returned by the callback on GET", async () => {
141+
const dynamicClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });
142+
dynamicClient.setGetHeaders(() => ({ authorization: "Bearer token-abc" }));
143+
const spy = mockFetch(200, {});
144+
await dynamicClient.get("/path");
145+
const calledHeaders = ((spy.mock.calls[0] as unknown[])[1] as RequestInit).headers as Record<string, string>;
146+
expect(calledHeaders["authorization"]).toBe("Bearer token-abc");
147+
});
148+
149+
it("includes dynamic header returned by the callback on POST", async () => {
150+
const dynamicClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });
151+
dynamicClient.setGetHeaders(() => ({ authorization: "Bearer token-post" }));
152+
const spy = mockFetch(200, {});
153+
await dynamicClient.post("/path", {});
154+
const calledHeaders = ((spy.mock.calls[0] as unknown[])[1] as RequestInit).headers as Record<string, string>;
155+
expect(calledHeaders["authorization"]).toBe("Bearer token-post");
156+
});
157+
158+
it("calls the callback on every request so token changes are reflected", async () => {
159+
const dynamicClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });
160+
let token = "token-v1";
161+
dynamicClient.setGetHeaders(() => ({ authorization: `Bearer ${token}` }));
162+
163+
const spy = mockFetch(200, {});
164+
await dynamicClient.get("/path");
165+
const firstHeaders = ((spy.mock.calls[0] as unknown[])[1] as RequestInit).headers as Record<string, string>;
166+
expect(firstHeaders["authorization"]).toBe("Bearer token-v1");
167+
168+
token = "token-v2";
169+
await dynamicClient.get("/path");
170+
const secondHeaders = ((spy.mock.calls[1] as unknown[])[1] as RequestInit).headers as Record<string, string>;
171+
expect(secondHeaders["authorization"]).toBe("Bearer token-v2");
172+
});
173+
174+
it("omits dynamic header keys whose value is undefined", async () => {
175+
const dynamicClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });
176+
dynamicClient.setGetHeaders(() => ({ authorization: "Bearer token", "x-optional": undefined }));
177+
const spy = mockFetch(200, {});
178+
await dynamicClient.get("/path");
179+
const calledHeaders = ((spy.mock.calls[0] as unknown[])[1] as RequestInit).headers as Record<string, string>;
180+
expect(calledHeaders["authorization"]).toBe("Bearer token");
181+
expect(calledHeaders["x-optional"]).toBeUndefined();
182+
});
183+
184+
it("dynamic headers override static headers with the same key", async () => {
185+
const dynamicClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });
186+
dynamicClient.setHeaders({ authorization: "Bearer static-token" });
187+
dynamicClient.setGetHeaders(() => ({ authorization: "Bearer dynamic-token" }));
188+
const spy = mockFetch(200, {});
189+
await dynamicClient.get("/path");
190+
const calledHeaders = ((spy.mock.calls[0] as unknown[])[1] as RequestInit).headers as Record<string, string>;
191+
expect(calledHeaders["authorization"]).toBe("Bearer dynamic-token");
192+
});
193+
194+
it("per-request extra headers override dynamic headers", async () => {
195+
const dynamicClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });
196+
dynamicClient.setGetHeaders(() => ({ "x-custom": "from-dynamic" }));
197+
const spy = mockFetch(200, {});
198+
await dynamicClient.get("/path", { headers: { "x-custom": "from-extra" } });
199+
const calledHeaders = ((spy.mock.calls[0] as unknown[])[1] as RequestInit).headers as Record<string, string>;
200+
expect(calledHeaders["x-custom"]).toBe("from-extra");
201+
});
202+
});
203+
139204
describe("setPaymentSignature()", () => {
140205
it("includes X-Payment header in subsequent requests after setPaymentSignature", async () => {
141206
const payingClient = new PhantomApiClient({ baseUrl: "https://api.phantom.app" });

packages/phantom-openclaw-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@phantom/phantom-openclaw-plugin",
3-
"version": "1.0.0-beta.0",
3+
"version": "1.0.0",
44
"description": "Phantom Embedded Wallet — sign transactions, transfer tokens, and swap on Solana and Ethereum. Powered by Phantom.",
55
"repository": {
66
"type": "git",

yarn.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5541,7 +5541,6 @@ __metadata:
55415541
"@phantom/perps-client": "workspace:^"
55425542
"@phantom/phantom-api-client": "workspace:^"
55435543
"@phantom/sdk-types": "workspace:^"
5544-
"@phantom/server-sdk": "workspace:^"
55455544
"@phantom/utils": "workspace:^"
55465545
"@solana/spl-token": "npm:^0.4.14"
55475546
"@solana/web3.js": "npm:^1.95.5"

0 commit comments

Comments
 (0)