Skip to content

Commit e7cf2f1

Browse files
committed
Merge branch 'custom-headers'
2 parents 5a31434 + d7c14aa commit e7cf2f1

File tree

13 files changed

+316
-251
lines changed

13 files changed

+316
-251
lines changed

.github/workflows/check.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Check
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
check:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Setup pnpm & install
17+
uses: wyvox/action-setup-pnpm@v3
18+
with:
19+
node-version: 22
20+
21+
- name: Build
22+
run: pnpm build
23+
24+
- name: Run checks
25+
run: pnpm run check

.github/workflows/publish.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Publish Any Commit
2+
on:
3+
pull_request:
4+
push:
5+
branches:
6+
- '**'
7+
tags:
8+
- '!**'
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Setup pnpm & install
19+
uses: wyvox/action-setup-pnpm@v3
20+
with:
21+
node-version: 22
22+
23+
- name: Build
24+
run: pnpm build
25+
26+
- run: pnpm dlx pkg-pr-new publish --bin

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
pnpm-lock.yaml
1+
pnpm-lock.yaml
2+
*.md

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "use-mcp",
3+
"repository": "https://github.com/geelen/use-mcp",
34
"version": "0.0.7",
45
"type": "module",
56
"files": [
@@ -50,5 +51,6 @@
5051
"react",
5152
"@modelcontextprotocol/sdk"
5253
]
53-
}
54+
},
55+
"packageManager": "[email protected]+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
5456
}

src/auth/browser-provider.ts

Lines changed: 92 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
// browser-provider.ts
2-
import { OAuthClientInformation, OAuthMetadata, OAuthTokens, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js';
3-
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
2+
import { OAuthClientInformation, OAuthMetadata, OAuthTokens, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
3+
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
44
// Assuming StoredState is defined in ./types.js and includes fields for provider options
5-
import { StoredState } from './types.js'; // Adjust path if necessary
5+
import { StoredState } from './types.js' // Adjust path if necessary
66

77
/**
88
* Browser-compatible OAuth client provider for MCP using localStorage.
99
*/
1010
export class BrowserOAuthClientProvider implements OAuthClientProvider {
11-
readonly serverUrl: string;
12-
readonly storageKeyPrefix: string;
13-
readonly serverUrlHash: string;
14-
readonly clientName: string;
15-
readonly clientUri: string;
16-
readonly callbackUrl: string;
11+
readonly serverUrl: string
12+
readonly storageKeyPrefix: string
13+
readonly serverUrlHash: string
14+
readonly clientName: string
15+
readonly clientUri: string
16+
readonly callbackUrl: string
1717

1818
constructor(
1919
serverUrl: string,
2020
options: {
21-
storageKeyPrefix?: string;
22-
clientName?: string;
23-
clientUri?: string;
24-
callbackUrl?: string;
21+
storageKeyPrefix?: string
22+
clientName?: string
23+
clientUri?: string
24+
callbackUrl?: string
2525
} = {},
2626
) {
27-
this.serverUrl = serverUrl;
28-
this.storageKeyPrefix = options.storageKeyPrefix || 'mcp:auth';
29-
this.serverUrlHash = this.hashString(serverUrl);
30-
this.clientName = options.clientName || 'MCP Browser Client';
31-
this.clientUri = options.clientUri || (typeof window !== 'undefined' ? window.location.origin : '');
32-
this.callbackUrl = options.callbackUrl || (typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback');
27+
this.serverUrl = serverUrl
28+
this.storageKeyPrefix = options.storageKeyPrefix || 'mcp:auth'
29+
this.serverUrlHash = this.hashString(serverUrl)
30+
this.clientName = options.clientName || 'MCP Browser Client'
31+
this.clientUri = options.clientUri || (typeof window !== 'undefined' ? window.location.origin : '')
32+
this.callbackUrl =
33+
options.callbackUrl ||
34+
(typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback')
3335
}
3436

3537
// --- SDK Interface Methods ---
3638

3739
get redirectUrl(): string {
38-
return this.callbackUrl;
40+
return this.callbackUrl
3941
}
4042

4143
get clientMetadata(): OAuthClientMetadata {
@@ -47,68 +49,69 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
4749
client_name: this.clientName,
4850
client_uri: this.clientUri,
4951
// scope: 'openid profile email mcp', // Example scopes, adjust as needed
50-
};
52+
}
5153
}
5254

5355
async clientInformation(): Promise<OAuthClientInformation | undefined> {
54-
const key = this.getKey('client_info');
55-
const data = localStorage.getItem(key);
56-
if (!data) return undefined;
56+
const key = this.getKey('client_info')
57+
const data = localStorage.getItem(key)
58+
if (!data) return undefined
5759
try {
5860
// TODO: Add validation using a schema
59-
return JSON.parse(data) as OAuthClientInformation;
61+
return JSON.parse(data) as OAuthClientInformation
6062
} catch (e) {
61-
console.warn(`[${this.storageKeyPrefix}] Failed to parse client information:`, e);
62-
localStorage.removeItem(key);
63-
return undefined;
63+
console.warn(`[${this.storageKeyPrefix}] Failed to parse client information:`, e)
64+
localStorage.removeItem(key)
65+
return undefined
6466
}
6567
}
6668

6769
// NOTE: The SDK's auth() function uses this if dynamic registration is needed.
6870
// Ensure your OAuthClientInformationFull matches the expected structure if DCR is used.
6971
async saveClientInformation(clientInformation: OAuthClientInformation /* | OAuthClientInformationFull */): Promise<void> {
70-
const key = this.getKey('client_info');
72+
const key = this.getKey('client_info')
7173
// Cast needed if handling OAuthClientInformationFull specifically
72-
localStorage.setItem(key, JSON.stringify(clientInformation));
74+
localStorage.setItem(key, JSON.stringify(clientInformation))
7375
}
7476

75-
7677
async tokens(): Promise<OAuthTokens | undefined> {
77-
const key = this.getKey('tokens');
78-
const data = localStorage.getItem(key);
79-
if (!data) return undefined;
78+
const key = this.getKey('tokens')
79+
const data = localStorage.getItem(key)
80+
if (!data) return undefined
8081
try {
8182
// TODO: Add validation
82-
return JSON.parse(data) as OAuthTokens;
83+
return JSON.parse(data) as OAuthTokens
8384
} catch (e) {
84-
console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e);
85-
localStorage.removeItem(key);
86-
return undefined;
85+
console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e)
86+
localStorage.removeItem(key)
87+
return undefined
8788
}
8889
}
8990

9091
async saveTokens(tokens: OAuthTokens): Promise<void> {
91-
const key = this.getKey('tokens');
92-
localStorage.setItem(key, JSON.stringify(tokens));
92+
const key = this.getKey('tokens')
93+
localStorage.setItem(key, JSON.stringify(tokens))
9394
// Clean up code verifier and last auth URL after successful token save
94-
localStorage.removeItem(this.getKey('code_verifier'));
95-
localStorage.removeItem(this.getKey('last_auth_url'));
95+
localStorage.removeItem(this.getKey('code_verifier'))
96+
localStorage.removeItem(this.getKey('last_auth_url'))
9697
}
9798

9899
async saveCodeVerifier(codeVerifier: string): Promise<void> {
99-
const key = this.getKey('code_verifier');
100-
localStorage.setItem(key, codeVerifier);
100+
const key = this.getKey('code_verifier')
101+
localStorage.setItem(key, codeVerifier)
101102
}
102103

103104
async codeVerifier(): Promise<string> {
104-
const key = this.getKey('code_verifier');
105-
const verifier = localStorage.getItem(key);
105+
const key = this.getKey('code_verifier')
106+
const verifier = localStorage.getItem(key)
106107
if (!verifier) {
107-
throw new Error(`[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`);
108+
throw new Error(
109+
`[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`,
110+
)
108111
}
109112
// SDK's auth() retrieves this BEFORE exchanging code. Don't remove it here.
110113
// It will be removed in saveTokens on success.
111-
return verifier;
114+
return verifier
112115
}
113116

114117
/**
@@ -118,8 +121,8 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
118121
*/
119122
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
120123
// Generate a unique state parameter for this authorization request
121-
const state = crypto.randomUUID();
122-
const stateKey = `${this.storageKeyPrefix}:state_${state}`;
124+
const state = crypto.randomUUID()
125+
const stateKey = `${this.storageKeyPrefix}:state_${state}`
123126

124127
// Store context needed by the callback handler, associated with the state param
125128
const stateData: StoredState = {
@@ -132,32 +135,34 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
132135
clientName: this.clientName,
133136
clientUri: this.clientUri,
134137
callbackUrl: this.callbackUrl,
135-
}
136-
};
137-
localStorage.setItem(stateKey, JSON.stringify(stateData));
138+
},
139+
}
140+
localStorage.setItem(stateKey, JSON.stringify(stateData))
138141

139142
// Add the state parameter to the URL
140-
authorizationUrl.searchParams.set('state', state);
141-
const authUrlString = authorizationUrl.toString();
143+
authorizationUrl.searchParams.set('state', state)
144+
const authUrlString = authorizationUrl.toString()
142145

143146
// Persist the exact auth URL in case the popup fails and manual navigation is needed
144-
localStorage.setItem(this.getKey('last_auth_url'), authUrlString);
147+
localStorage.setItem(this.getKey('last_auth_url'), authUrlString)
145148

146149
// Attempt to open the popup
147-
const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes'; // Make configurable if needed
150+
const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' // Make configurable if needed
148151
try {
149-
const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures);
152+
const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures)
150153

151154
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
152-
console.warn(`[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`);
155+
console.warn(
156+
`[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`,
157+
)
153158
// Cannot signal failure back via SDK auth() directly.
154159
// useMcp will need to rely on timeout or manual trigger if stuck.
155160
} else {
156-
popup.focus();
157-
console.info(`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`);
161+
popup.focus()
162+
console.info(`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`)
158163
}
159164
} catch (e) {
160-
console.error(`[${this.storageKeyPrefix}] Error opening popup window:`, e);
165+
console.error(`[${this.storageKeyPrefix}] Error opening popup window:`, e)
161166
// Cannot signal failure back via SDK auth() directly.
162167
}
163168
// Regardless of popup success, the interface expects this method to initiate the redirect.
@@ -170,60 +175,59 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider {
170175
* Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback.
171176
*/
172177
getLastAttemptedAuthUrl(): string | null {
173-
return localStorage.getItem(this.getKey('last_auth_url'));
178+
return localStorage.getItem(this.getKey('last_auth_url'))
174179
}
175180

176-
177181
clearStorage(): number {
178-
const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`;
179-
const statePattern = `${this.storageKeyPrefix}:state_`;
180-
const keysToRemove: string[] = [];
181-
let count = 0;
182+
const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`
183+
const statePattern = `${this.storageKeyPrefix}:state_`
184+
const keysToRemove: string[] = []
185+
let count = 0
182186

183187
for (let i = 0; i < localStorage.length; i++) {
184-
const key = localStorage.key(i);
185-
if (!key) continue;
188+
const key = localStorage.key(i)
189+
if (!key) continue
186190

187191
if (key.startsWith(prefixPattern)) {
188-
keysToRemove.push(key);
192+
keysToRemove.push(key)
189193
} else if (key.startsWith(statePattern)) {
190194
try {
191-
const item = localStorage.getItem(key);
195+
const item = localStorage.getItem(key)
192196
if (item) {
193197
// Check if state belongs to this provider instance based on serverUrlHash
194198
// We need to parse cautiously as the structure isn't guaranteed.
195-
const state = JSON.parse(item) as Partial<StoredState>;
199+
const state = JSON.parse(item) as Partial<StoredState>
196200
if (state.serverUrlHash === this.serverUrlHash) {
197-
keysToRemove.push(key);
201+
keysToRemove.push(key)
198202
}
199203
}
200204
} catch (e) {
201-
console.warn(`[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`, e);
205+
console.warn(`[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`, e)
202206
// Optionally remove malformed keys
203207
// keysToRemove.push(key);
204208
}
205209
}
206210
}
207211

208-
const uniqueKeysToRemove = [...new Set(keysToRemove)];
209-
uniqueKeysToRemove.forEach(key => {
210-
localStorage.removeItem(key);
211-
count++;
212-
});
213-
return count;
212+
const uniqueKeysToRemove = [...new Set(keysToRemove)]
213+
uniqueKeysToRemove.forEach((key) => {
214+
localStorage.removeItem(key)
215+
count++
216+
})
217+
return count
214218
}
215219

216220
private hashString(str: string): string {
217-
let hash = 0;
221+
let hash = 0
218222
for (let i = 0; i < str.length; i++) {
219-
const char = str.charCodeAt(i);
220-
hash = (hash << 5) - hash + char;
221-
hash = hash & hash;
223+
const char = str.charCodeAt(i)
224+
hash = (hash << 5) - hash + char
225+
hash = hash & hash
222226
}
223-
return Math.abs(hash).toString(16);
227+
return Math.abs(hash).toString(16)
224228
}
225229

226230
getKey(keySuffix: string): string {
227-
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`;
231+
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`
228232
}
229-
}
233+
}

0 commit comments

Comments
 (0)