Skip to content

Commit 6c5a96b

Browse files
committed
automatic skipLocalStorage for server and client if database
1 parent f799271 commit 6c5a96b

File tree

10 files changed

+662
-38
lines changed

10 files changed

+662
-38
lines changed

index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ import { notionIntegration } from './src/integrations/notion.js';
4545
* Default configuration:
4646
* - Calls API routes at: {window.location.origin}/api/integrate/mcp
4747
* - OAuth routes at: {window.location.origin}/api/integrate/oauth/*
48-
* - Saves tokens to localStorage (tokens persist across page reloads)
48+
* - Automatically detects if server uses database storage and skips localStorage accordingly
4949
*
50-
* For custom configuration (different apiBaseUrl, apiRouteBase, localStorage, etc.),
50+
* For custom configuration (different apiBaseUrl, apiRouteBase, etc.),
5151
* use `createMCPClient()` instead.
5252
*
5353
* @example
@@ -68,7 +68,6 @@ import { notionIntegration } from './src/integrations/notion.js';
6868
*
6969
* const customClient = createMCPClient({
7070
* integrations: [githubIntegration()],
71-
* skipLocalStorage: true, // Disable localStorage and use server-side callbacks
7271
* });
7372
* ```
7473
*/
@@ -78,8 +77,5 @@ export const client = createMCPClient({
7877
gmailIntegration(),
7978
notionIntegration(),
8079
],
81-
// Default client uses localStorage to persist tokens across page reloads
82-
// If you need server-side token management, set skipLocalStorage: true and configure callbacks
83-
skipLocalStorage: false,
8480
});
8581

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "integrate-sdk",
3-
"version": "0.8.23",
3+
"version": "0.8.24",
44
"description": "Type-safe 3rd party integration SDK for the Integrate MCP server",
55
"type": "module",
66
"main": "./dist/index.js",

src/adapters/base-handler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ export class OAuthHandler {
231231
return headers;
232232
}
233233

234+
/**
235+
* Check if database callbacks are configured
236+
* Used to determine if we should set X-Integrate-Use-Database header
237+
*/
238+
hasDatabaseCallbacks(): boolean {
239+
return !!this.config.setProviderToken;
240+
}
241+
234242
/**
235243
* Handle authorization URL request
236244
* Gets authorization URL from MCP server with full OAuth credentials

src/adapters/nextjs.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ export function createNextOAuthHandler(config: OAuthHandlerConfig) {
115115
response.headers.set('Set-Cookie', result.setCookie);
116116
}
117117

118+
// Add X-Integrate-Use-Database header if database callbacks are configured
119+
if (handler.hasDatabaseCallbacks()) {
120+
response.headers.set('X-Integrate-Use-Database', 'true');
121+
}
122+
118123
return response;
119124
} catch (error: any) {
120125
console.error('[OAuth Authorize] Error:', error);
@@ -180,6 +185,11 @@ export function createNextOAuthHandler(config: OAuthHandlerConfig) {
180185
response.headers.set('Set-Cookie', result.clearCookie);
181186
}
182187

188+
// Add X-Integrate-Use-Database header if database callbacks are configured
189+
if (handler.hasDatabaseCallbacks()) {
190+
response.headers.set('X-Integrate-Use-Database', 'true');
191+
}
192+
183193
return response;
184194
} catch (error: any) {
185195
console.error('[OAuth Callback] Error:', error);
@@ -248,7 +258,14 @@ export function createNextOAuthHandler(config: OAuthHandlerConfig) {
248258

249259
const accessToken = authHeader.substring(7); // Remove 'Bearer ' prefix
250260
const result = await handler.handleStatus(provider, accessToken);
251-
return Response.json(result);
261+
const response = Response.json(result);
262+
263+
// Add X-Integrate-Use-Database header if database callbacks are configured
264+
if (handler.hasDatabaseCallbacks()) {
265+
response.headers.set('X-Integrate-Use-Database', 'true');
266+
}
267+
268+
return response;
252269
} catch (error: any) {
253270
console.error('[OAuth Status] Error:', error);
254271
return Response.json(
@@ -322,7 +339,14 @@ export function createNextOAuthHandler(config: OAuthHandlerConfig) {
322339

323340
// Pass the request object for context extraction
324341
const result = await handler.handleDisconnect({ provider }, accessToken, req);
325-
return Response.json(result);
342+
const response = Response.json(result);
343+
344+
// Add X-Integrate-Use-Database header if database callbacks are configured
345+
if (handler.hasDatabaseCallbacks()) {
346+
response.headers.set('X-Integrate-Use-Database', 'true');
347+
}
348+
349+
return response;
326350
} catch (error: any) {
327351
console.error('[OAuth Disconnect] Error:', error);
328352
return Response.json(

src/client.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export class MCPClientBase<TIntegrations extends readonly MCPIntegration[] = rea
160160
private eventEmitter: SimpleEventEmitter = new SimpleEventEmitter();
161161
private apiRouteBase: string;
162162
private apiBaseUrl?: string;
163+
private databaseDetected: boolean = false;
163164

164165
/**
165166
* Promise that resolves when OAuth callback processing is complete
@@ -222,7 +223,6 @@ export class MCPClientBase<TIntegrations extends readonly MCPIntegration[] = rea
222223
getProviderToken: (config as any).getProviderToken,
223224
setProviderToken: (config as any).setProviderToken,
224225
removeProviderToken: (config as any).removeProviderToken,
225-
skipLocalStorage: config.skipLocalStorage,
226226
}
227227
);
228228

@@ -620,6 +620,12 @@ export class MCPClientBase<TIntegrations extends readonly MCPIntegration[] = rea
620620
}),
621621
});
622622

623+
// Check for X-Integrate-Use-Database header to auto-detect database usage
624+
if (!this.databaseDetected && response.headers.get('X-Integrate-Use-Database') === 'true') {
625+
this.oauthManager.setSkipLocalStorage(true);
626+
this.databaseDetected = true;
627+
}
628+
623629
if (!response.ok) {
624630
// Try to parse error response
625631
let errorMessage = `Request failed: ${response.statusText}`;

src/config/types.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -449,26 +449,6 @@ export interface MCPClientConfig<TIntegrations extends readonly MCPIntegration[]
449449
*/
450450
autoHandleOAuthCallback?: boolean;
451451

452-
/**
453-
* Skip saving OAuth tokens to localStorage
454-
* Set to true when using server-side database storage for tokens
455-
*
456-
* When true, tokens will not be saved to browser localStorage.
457-
* This is useful when your server handles all token storage via database callbacks.
458-
*
459-
* @default false (tokens are saved to localStorage)
460-
*
461-
* @example
462-
* ```typescript
463-
* // Browser client when server uses database storage
464-
* const client = createMCPClient({
465-
* integrations: [githubIntegration({ ... })],
466-
* skipLocalStorage: true // Don't save tokens in browser
467-
* });
468-
* ```
469-
*/
470-
skipLocalStorage?: boolean;
471-
472452
/**
473453
* Configure behavior when OAuth callback processing fails
474454
*

src/oauth/manager.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class OAuthManager {
4040
getProviderToken?: (provider: string, context?: MCPContext) => Promise<ProviderTokenData | undefined> | ProviderTokenData | undefined;
4141
setProviderToken?: (provider: string, tokenData: ProviderTokenData | null, context?: MCPContext) => Promise<void> | void;
4242
removeProviderToken?: (provider: string, context?: MCPContext) => Promise<void> | void;
43-
skipLocalStorage?: boolean;
4443
}
4544
) {
4645
this.oauthApiBase = oauthApiBase;
@@ -55,12 +54,10 @@ export class OAuthManager {
5554
this.setTokenCallback = tokenCallbacks?.setProviderToken;
5655
this.removeTokenCallback = tokenCallbacks?.removeProviderToken;
5756

58-
// Determine skipLocalStorage setting:
59-
// 1. If explicitly set, use that value
60-
// 2. If getTokenCallback is provided (indicating server-side database storage), auto-set to true
61-
// 3. Otherwise, default to false (use localStorage when callbacks are not configured)
62-
// This ensures localStorage is only used when callbacks are NOT configured AND skipLocalStorage is false
63-
this.skipLocalStorage = tokenCallbacks?.skipLocalStorage ?? !!tokenCallbacks?.getProviderToken;
57+
// Auto-detect skipLocalStorage from callbacks:
58+
// If getTokenCallback or setTokenCallback is provided (indicating server-side database storage), auto-set to true
59+
// Otherwise, default to false (use localStorage when callbacks are not configured)
60+
this.skipLocalStorage = !!(tokenCallbacks?.getProviderToken || tokenCallbacks?.setProviderToken);
6461

6562
// Clean up any expired pending auth entries from localStorage
6663
this.cleanupExpiredPendingAuths();
@@ -843,6 +840,11 @@ export class OAuthManager {
843840
}),
844841
});
845842

843+
// Check for X-Integrate-Use-Database header to auto-detect database usage
844+
if (response.headers.get('X-Integrate-Use-Database') === 'true') {
845+
this.skipLocalStorage = true;
846+
}
847+
846848
if (!response.ok) {
847849
const error = await response.text();
848850
throw new Error(`Failed to get authorization URL: ${error}`);
@@ -899,6 +901,11 @@ export class OAuthManager {
899901
}),
900902
});
901903

904+
// Check for X-Integrate-Use-Database header to auto-detect database usage
905+
if (response.headers.get('X-Integrate-Use-Database') === 'true') {
906+
this.skipLocalStorage = true;
907+
}
908+
902909
if (!response.ok) {
903910
const error = await response.text();
904911
throw new Error(`Failed to exchange code for token: ${error}`);
@@ -908,6 +915,14 @@ export class OAuthManager {
908915
return data;
909916
}
910917

918+
/**
919+
* Update skipLocalStorage setting at runtime
920+
* Called automatically when server indicates database usage via response header
921+
*/
922+
setSkipLocalStorage(value: boolean): void {
923+
this.skipLocalStorage = value;
924+
}
925+
911926
/**
912927
* Close any open OAuth windows
913928
*/

src/server.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,20 @@ export function createMCPServer<TIntegrations extends readonly MCPIntegration[]>
397397
providers,
398398
serverUrl: config.serverUrl,
399399
apiKey: config.apiKey, // API key is included here and sent to MCP server
400+
setProviderToken: config.setProviderToken,
401+
removeProviderToken: config.removeProviderToken,
402+
getSessionContext: config.getSessionContext,
400403
});
401404

402405
const result = await oauthHandler.handleToolCall(body, authHeader);
403-
return Response.json(result);
406+
const response = Response.json(result);
407+
408+
// Add X-Integrate-Use-Database header if database callbacks are configured
409+
if (oauthHandler.hasDatabaseCallbacks()) {
410+
response.headers.set('X-Integrate-Use-Database', 'true');
411+
}
412+
413+
return response;
404414
} catch (error: any) {
405415
console.error('[MCP Tool Call] Error:', error);
406416
return Response.json(

tests/client/api-routing.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,5 +465,93 @@ describe("MCP Client - API Routing", () => {
465465

466466
expect(apiHandlerCalled).toBe(true);
467467
});
468+
469+
it("should detect X-Integrate-Use-Database header and update skipLocalStorage", async () => {
470+
const mockLocalStorage = new Map<string, string>();
471+
472+
// Mock localStorage
473+
global.localStorage = {
474+
getItem: (key: string) => mockLocalStorage.get(key) || null,
475+
setItem: (key: string, value: string) => mockLocalStorage.set(key, value),
476+
removeItem: (key: string) => mockLocalStorage.delete(key),
477+
clear: () => mockLocalStorage.clear(),
478+
length: 0,
479+
key: () => null,
480+
} as any;
481+
482+
let headerDetected = false;
483+
484+
global.fetch = mock(async (url: string, options?: any) => {
485+
if (url.includes("/api/integrate/mcp")) {
486+
// Return response with X-Integrate-Use-Database header
487+
const headers = new Headers();
488+
headers.set('X-Integrate-Use-Database', 'true');
489+
490+
return {
491+
ok: true,
492+
headers,
493+
json: async () => ({
494+
content: [{ type: "text", text: "result" }],
495+
}),
496+
} as Response;
497+
} else if (url.includes("mcp.integrate.dev")) {
498+
// Mock initialization calls
499+
return {
500+
ok: true,
501+
headers: new Headers(),
502+
json: async () => ({
503+
jsonrpc: "2.0",
504+
id: 1,
505+
result: { tools: [] },
506+
}),
507+
} as Response;
508+
}
509+
return { ok: false, headers: new Headers() } as Response;
510+
}) as any;
511+
512+
const mockOAuthManager = {
513+
getProviderToken: mock(() => null),
514+
loadAllProviderTokens: mock(() => { }),
515+
getAllProviderTokens: mock(() => new Map()),
516+
setProviderToken: mock(() => { }),
517+
clearAllProviderTokens: mock(() => { }),
518+
clearAllPendingAuths: mock(() => { }),
519+
checkAuthStatus: mock(() => Promise.resolve({ authorized: false })),
520+
initiateFlow: mock(() => Promise.resolve()),
521+
handleCallback: mock(() => Promise.resolve({ provider: "github", accessToken: "token", expiresAt: Date.now() })),
522+
disconnectProvider: mock(() => Promise.resolve()),
523+
setSkipLocalStorage: mock((value: boolean) => {
524+
headerDetected = value;
525+
}),
526+
};
527+
528+
const integration = createSimpleIntegration({
529+
id: "test",
530+
tools: ["test_tool"],
531+
});
532+
533+
const client = new MCPClientBase({
534+
integrations: [integration],
535+
apiRouteBase: "/api/integrate",
536+
connectionMode: "manual",
537+
});
538+
539+
(client as any).oauthManager = mockOAuthManager;
540+
(client as any).initialized = true;
541+
(client as any).transport.connected = true;
542+
(client as any).availableTools.set("test_tool", {
543+
name: "test_tool",
544+
description: "Test tool",
545+
inputSchema: { type: "object" },
546+
});
547+
(client as any).enabledToolNames.add("test_tool");
548+
549+
// Make a tool call - should detect header
550+
await (client as any).callServerTool("test_tool", {});
551+
552+
// Verify header was detected and skipLocalStorage was set
553+
expect(mockOAuthManager.setSkipLocalStorage).toHaveBeenCalledWith(true);
554+
expect(headerDetected).toBe(true);
555+
});
468556
});
469557

0 commit comments

Comments
 (0)