Skip to content

Commit db92c69

Browse files
max-stytchclaude
andauthored
fix: OAuth nits, run typecheck and linter in CI (#94)
* Replace expires_at with expires_in in OAuth tokens and fix TypeScript errors - Replace non-standard expires_at with standard expires_in field for OAuth access tokens - Update token validation logic to check expires_in as number instead of parsing timestamps - Fix TypeScript compilation errors by adding any type annotations to catch blocks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Apply Prettier formatting and code style improvements - Update GitHub workflow quote style consistency - Format README with consistent list markers and spacing - Add lint-fix script to package.json - Apply consistent formatting across TypeScript source files - Standardize quote usage, indentation, and spacing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Add GitHub workflow to run checks on PRs to main branch - Run 'pnpm build' and 'pnpm run check' on every pull request to main - Uses same pnpm setup as existing publish workflow for consistency - Ensures code quality and type checking before merging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * remove npm package-lock, we use pnpm in this house * fix lints again! --------- Co-authored-by: Claude <[email protected]>
1 parent 1c52215 commit db92c69

File tree

7 files changed

+124
-111
lines changed

7 files changed

+124
-111
lines changed

.github/workflows/check.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
pnpm-version: 10
21+
22+
- name: Build
23+
run: pnpm build
24+
25+
- name: Run checks
26+
run: pnpm run check

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ on:
33
pull_request:
44
push:
55
branches:
6-
- "**"
6+
- '**'
77
tags:
8-
- "!**"
8+
- '!**'
99

1010
jobs:
1111
build:

README.md

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ All the most popular MCP clients (Claude Desktop, Cursor & Windsurf) use the fol
2323
"mcpServers": {
2424
"remote-example": {
2525
"command": "npx",
26-
"args": [
27-
"mcp-remote",
28-
"https://remote.mcp.server/sse"
29-
]
26+
"args": ["mcp-remote", "https://remote.mcp.server/sse"]
3027
}
3128
}
3229
}
@@ -41,16 +38,11 @@ To bypass authentication, or to emit custom headers on all requests to your remo
4138
"mcpServers": {
4239
"remote-example": {
4340
"command": "npx",
44-
"args": [
45-
"mcp-remote",
46-
"https://remote.mcp.server/sse",
47-
"--header",
48-
"Authorization: Bearer ${AUTH_TOKEN}"
49-
],
41+
"args": ["mcp-remote", "https://remote.mcp.server/sse", "--header", "Authorization: Bearer ${AUTH_TOKEN}"],
5042
"env": {
5143
"AUTH_TOKEN": "..."
5244
}
53-
},
45+
}
5446
}
5547
}
5648
```
@@ -74,7 +66,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
7466

7567
### Flags
7668

77-
* If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
69+
- If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
7870

7971
```json
8072
"command": "npx",
@@ -85,7 +77,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
8577
]
8678
```
8779

88-
* To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
80+
- To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
8981

9082
```json
9183
"args": [
@@ -94,7 +86,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
9486
]
9587
```
9688

97-
* To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random.
89+
- To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random.
9890

9991
```json
10092
"args": [
@@ -104,7 +96,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
10496
]
10597
```
10698

107-
* To change which host `mcp-remote` registers as the OAuth callback URL (by default `localhost`), add the `--host` flag.
99+
- To change which host `mcp-remote` registers as the OAuth callback URL (by default `localhost`), add the `--host` flag.
108100

109101
```json
110102
"args": [
@@ -115,7 +107,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
115107
]
116108
```
117109

118-
* To allow HTTP connections in trusted private networks, add the `--allow-http` flag. Note: This should only be used in secure private networks where traffic cannot be intercepted.
110+
- To allow HTTP connections in trusted private networks, add the `--allow-http` flag. Note: This should only be used in secure private networks where traffic cannot be intercepted.
119111

120112
```json
121113
"args": [
@@ -125,7 +117,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
125117
]
126118
```
127119

128-
* To enable detailed debugging logs, add the `--debug` flag. This will write verbose logs to `~/.mcp-auth/{server_hash}_debug.log` with timestamps and detailed information about the auth process, connections, and token refreshing.
120+
- To enable detailed debugging logs, add the `--debug` flag. This will write verbose logs to `~/.mcp-auth/{server_hash}_debug.log` with timestamps and detailed information about the auth process, connections, and token refreshing.
129121

130122
```json
131123
"args": [
@@ -189,8 +181,8 @@ npx mcp-remote https://example.remote/server --static-oauth-client-info '@/Users
189181

190182
In order to add an MCP server to Claude Desktop you need to edit the configuration file located at:
191183

192-
* macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
193-
* Windows: `%APPDATA%\Claude\claude_desktop_config.json`
184+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
185+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
194186

195187
If it does not exist yet, [you may need to enable it under Settings > Developer](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server).
196188

@@ -212,16 +204,16 @@ As of version `0.48.0`, Cursor supports unauthed SSE servers directly. If your M
212204

213205
For instructions on building & deploying remote MCP servers, including acting as a valid OAuth client, see the following resources:
214206

215-
* https://developers.cloudflare.com/agents/guides/remote-mcp-server/
207+
- https://developers.cloudflare.com/agents/guides/remote-mcp-server/
216208

217209
In particular, see:
218210

219-
* https://github.com/cloudflare/workers-oauth-provider for defining an MCP-comlpiant OAuth server in Cloudflare Workers
220-
* https://github.com/cloudflare/agents/tree/main/examples/mcp for defining an `McpAgent` using the [`agents`](https://npmjs.com/package/agents) framework.
211+
- https://github.com/cloudflare/workers-oauth-provider for defining an MCP-comlpiant OAuth server in Cloudflare Workers
212+
- https://github.com/cloudflare/agents/tree/main/examples/mcp for defining an `McpAgent` using the [`agents`](https://npmjs.com/package/agents) framework.
221213

222214
For more information about testing these servers, see also:
223215

224-
* https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/
216+
- https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/
225217

226218
Know of more resources you'd like to share? Please add them to this Readme and send a PR!
227219

@@ -256,13 +248,10 @@ this might look like:
256248

257249
```json
258250
{
259-
"mcpServers": {
251+
"mcpServers": {
260252
"remote-example": {
261253
"command": "npx",
262-
"args": [
263-
"mcp-remote",
264-
"https://remote.mcp.server/sse"
265-
],
254+
"args": ["mcp-remote", "https://remote.mcp.server/sse"],
266255
"env": {
267256
"NODE_EXTRA_CA_CERTS": "{your CA certificate file path}.pem"
268257
}
@@ -273,10 +262,10 @@ this might look like:
273262

274263
### Check the logs
275264

276-
* [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)
277-
* MacOS / Linux:<br/>`tail -n 20 -F ~/Library/Logs/Claude/mcp*.log`
278-
* For bash on WSL:<br/>`tail -n 20 -f "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log"`
279-
* Powershell: <br/>`Get-Content "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log" -Wait -Tail 20`
265+
- [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)
266+
- MacOS / Linux:<br/>`tail -n 20 -F ~/Library/Logs/Claude/mcp*.log`
267+
- For bash on WSL:<br/>`tail -n 20 -f "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log"`
268+
- Powershell: <br/>`Get-Content "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log" -Wait -Tail 20`
280269

281270
## Debugging
282271

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"scripts": {
2626
"build": "tsup",
2727
"build:watch": "tsup --watch",
28-
"check": "prettier --check . && tsc"
28+
"check": "prettier --check . && tsc",
29+
"lint-fix": "prettier --check . --write"
2930
},
3031
"dependencies": {
3132
"express": "^4.21.2",

src/lib/coordination.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ export async function isPidRunning(pid: number): Promise<boolean> {
3232
*/
3333
export async function isLockValid(lockData: LockfileData): Promise<boolean> {
3434
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if lockfile is valid', lockData)
35-
35+
3636
// Check if the lockfile is too old (over 30 minutes)
3737
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
3838
if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) {
3939
log('Lockfile is too old')
40-
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Lockfile is too old', {
41-
age: Date.now() - lockData.timestamp,
42-
maxAge: MAX_LOCK_AGE
43-
})
40+
if (DEBUG)
41+
await debugLog(global.currentServerUrlHash!, 'Lockfile is too old', {
42+
age: Date.now() - lockData.timestamp,
43+
maxAge: MAX_LOCK_AGE,
44+
})
4445
return false
4546
}
4647

@@ -54,7 +55,7 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
5455
// Check if the endpoint is accessible
5556
try {
5657
if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if endpoint is accessible', { port: lockData.port })
57-
58+
5859
const controller = new AbortController()
5960
const timeout = setTimeout(() => controller.abort(), 1000)
6061

@@ -63,9 +64,10 @@ export async function isLockValid(lockData: LockfileData): Promise<boolean> {
6364
})
6465

6566
clearTimeout(timeout)
66-
67+
6768
const isValid = response.status === 200 || response.status === 202
68-
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Endpoint check result: ${isValid ? 'valid' : 'invalid'}`, { status: response.status })
69+
if (DEBUG)
70+
await debugLog(global.currentServerUrlHash!, `Endpoint check result: ${isValid ? 'valid' : 'invalid'}`, { status: response.status })
6971
return isValid
7072
} catch (error) {
7173
log(`Error connecting to auth server: ${(error as Error).message}`)
@@ -84,13 +86,13 @@ export async function waitForAuthentication(port: number): Promise<boolean> {
8486
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Waiting for authentication from server on port ${port}`)
8587

8688
try {
87-
let attempts = 0;
89+
let attempts = 0
8890
while (true) {
89-
attempts++;
91+
attempts++
9092
const url = `http://127.0.0.1:${port}/wait-for-auth`
9193
log(`Querying: ${url}`)
9294
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll attempt ${attempts}: ${url}`)
93-
95+
9496
try {
9597
const response = await fetch(url)
9698
if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll response status: ${response.status}`)
@@ -130,11 +132,7 @@ export async function waitForAuthentication(port: number): Promise<boolean> {
130132
* @param events The event emitter to use for signaling
131133
* @returns An AuthCoordinator object with an initializeAuth method
132134
*/
133-
export function createLazyAuthCoordinator(
134-
serverUrlHash: string,
135-
callbackPort: number,
136-
events: EventEmitter
137-
): AuthCoordinator {
135+
export function createLazyAuthCoordinator(serverUrlHash: string, callbackPort: number, events: EventEmitter): AuthCoordinator {
138136
let authState: { server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean } | null = null
139137

140138
return {
@@ -147,12 +145,12 @@ export function createLazyAuthCoordinator(
147145

148146
log('Initializing auth coordination on-demand')
149147
if (DEBUG) await debugLog(serverUrlHash, 'Initializing auth coordination on-demand', { serverUrlHash, callbackPort })
150-
148+
151149
// Initialize auth using the existing coordinateAuth logic
152150
authState = await coordinateAuth(serverUrlHash, callbackPort, events)
153151
if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth })
154152
return authState
155-
}
153+
},
156154
}
157155
}
158156

@@ -169,10 +167,10 @@ export async function coordinateAuth(
169167
events: EventEmitter,
170168
): Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }> {
171169
if (DEBUG) await debugLog(serverUrlHash, 'Coordinating authentication', { serverUrlHash, callbackPort })
172-
170+
173171
// Check for a lockfile (disabled on Windows for the time being)
174172
const lockData = process.platform === 'win32' ? null : await checkLockfile(serverUrlHash)
175-
173+
176174
if (DEBUG) {
177175
if (process.platform === 'win32') {
178176
await debugLog(serverUrlHash, 'Skipping lockfile check on Windows')
@@ -190,7 +188,7 @@ export async function coordinateAuth(
190188
// Try to wait for the authentication to complete
191189
if (DEBUG) await debugLog(serverUrlHash, 'Waiting for authentication from other instance')
192190
const authCompleted = await waitForAuthentication(lockData.port)
193-
191+
194192
if (authCompleted) {
195193
log('Authentication completed by another instance')
196194
if (DEBUG) await debugLog(serverUrlHash, 'Authentication completed by another instance, will use tokens from disk')

src/lib/node-oauth-client-provider.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
4141
}
4242

4343
get redirectUrl(): string {
44-
return `http://${this.options.host}:${this.options.callbackPort}${this.callbackPath}`;
44+
return `http://${this.options.host}:${this.options.callbackPort}${this.callbackPath}`
4545
}
4646

4747
get clientMetadata() {
@@ -68,7 +68,11 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
6868
if (DEBUG) await debugLog(this.serverUrlHash, 'Returning static client info')
6969
return this.staticOAuthClientInfo
7070
}
71-
const clientInfo = await readJsonFile<OAuthClientInformationFull>(this.serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
71+
const clientInfo = await readJsonFile<OAuthClientInformationFull>(
72+
this.serverUrlHash,
73+
'client_info.json',
74+
OAuthClientInformationFullSchema,
75+
)
7276
if (DEBUG) await debugLog(this.serverUrlHash, 'Client info result:', clientInfo ? 'Found' : 'Not found')
7377
return clientInfo
7478
}
@@ -96,17 +100,14 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
96100

97101
if (DEBUG) {
98102
if (tokens) {
99-
const expiresAt = new Date(tokens.expires_at)
100-
const now = new Date()
101-
const expiresAtTime = expiresAt.getTime()
102-
const timeLeft = !isNaN(expiresAtTime) ? Math.round((expiresAtTime - now.getTime()) / 1000) : 0
103-
104-
// Alert if expires_at produces an invalid date
105-
if (isNaN(expiresAtTime)) {
106-
await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_at detected while reading tokens ⚠️', {
107-
expiresAt: tokens.expires_at,
103+
const timeLeft = tokens.expires_in || 0
104+
105+
// Alert if expires_in is invalid
106+
if (typeof tokens.expires_in !== 'number' || tokens.expires_in < 0) {
107+
await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_in detected while reading tokens ⚠️', {
108+
expiresIn: tokens.expires_in,
108109
tokenObject: JSON.stringify(tokens),
109-
stack: new Error('Invalid expires_at timestamp').stack
110+
stack: new Error('Invalid expires_in value').stack,
110111
})
111112
}
112113

@@ -116,7 +117,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
116117
hasRefreshToken: !!tokens.refresh_token,
117118
expiresIn: `${timeLeft} seconds`,
118119
isExpired: timeLeft <= 0,
119-
expiresAt: tokens.expires_at
120+
expiresInValue: tokens.expires_in,
120121
})
121122
} else {
122123
await debugLog(this.serverUrlHash, 'Token result: Not found')
@@ -132,25 +133,22 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
132133
*/
133134
async saveTokens(tokens: OAuthTokens): Promise<void> {
134135
if (DEBUG) {
135-
const expiresAt = new Date(tokens.expires_at)
136-
const now = new Date()
137-
const expiresAtTime = expiresAt.getTime()
138-
const timeLeft = !isNaN(expiresAtTime) ? Math.round((expiresAtTime - now.getTime()) / 1000) : 0
139-
140-
// Alert if expires_at produces an invalid date
141-
if (isNaN(expiresAtTime)) {
142-
await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_at detected in tokens ⚠️', {
143-
expiresAt: tokens.expires_at,
136+
const timeLeft = tokens.expires_in || 0
137+
138+
// Alert if expires_in is invalid
139+
if (typeof tokens.expires_in !== 'number' || tokens.expires_in < 0) {
140+
await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_in detected in tokens ⚠️', {
141+
expiresIn: tokens.expires_in,
144142
tokenObject: JSON.stringify(tokens),
145-
stack: new Error('Invalid expires_at timestamp').stack
143+
stack: new Error('Invalid expires_in value').stack,
146144
})
147145
}
148146

149147
await debugLog(this.serverUrlHash, 'Saving tokens', {
150148
hasAccessToken: !!tokens.access_token,
151149
hasRefreshToken: !!tokens.refresh_token,
152150
expiresIn: `${timeLeft} seconds`,
153-
expiresAt: tokens.expires_at
151+
expiresInValue: tokens.expires_in,
154152
})
155153
}
156154

0 commit comments

Comments
 (0)