-
Notifications
You must be signed in to change notification settings - Fork 0
Implement OAuth authentication and authorization / custom handlers fo… #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
cb04abb
3594df1
b2255dc
64137ee
59ebf07
1713220
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,116 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { readEnv } from './server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import express from 'express'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { fromError } from 'zod-validation-error/v3'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const BEEPER_DESKTOP_BASE_URL = readEnv('BEEPER_DESKTOP_BASE_URL') || 'http://localhost:23373'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const BEEPER_MCP_BASE_URL = readEnv('BEEPER_MCP_BASE_URL') || 'http://localhost:3000'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const BEEPER_AUTH_TOKEN = readEnv('AUTH_TOKEN') || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const createProxyProvider = (redirect_uris?: string[]): ProxyOAuthServerProvider => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new ProxyOAuthServerProvider({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
endpoints: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
authorizationUrl: `${BEEPER_DESKTOP_BASE_URL}/oauth/authorize`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
tokenUrl: `${BEEPER_DESKTOP_BASE_URL}/oauth/token`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
revocationUrl: `${BEEPER_DESKTOP_BASE_URL}/oauth/revoke`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
registrationUrl: `${BEEPER_DESKTOP_BASE_URL}/oauth/register`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
verifyAccessToken: async (token: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/v0/mcp/validate`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
method: 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Authorization: `Bearer ${token}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+19
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAdd network timeout and fix header/body mismatch on token validation. Prevent hangs; avoid sending Content-Type without a body. - verifyAccessToken: async (token: string) => {
- try {
- const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/v0/mcp/validate`, {
+ verifyAccessToken: async (token: string) => {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 10_000);
+ try {
+ const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/v0/mcp/validate`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
- 'Content-Type': 'application/json',
+ Accept: 'application/json',
},
- });
+ // no body; endpoint validates via Authorization header
+ signal: controller.signal,
+ });
console.log('Token validation response status:', response.status);
if (!response.ok) {
throw new Error(`invalid_token (status ${response.status})`);
}
const tokenData: any = await response.json();
return {
token,
clientId: tokenData.clientInfo?.name || 'unknown',
- scopes: tokenData.scopes || ['read'],
+ scopes: tokenData.scopes || [],
};
} catch (error) {
console.error('Token validation failed:', error);
throw error;
+ } finally {
+ clearTimeout(timeout);
}
}, Note: defaulting scopes to [] avoids implicitly granting “read”. If “read” is required as a fallback, ignore this part. Also applies to: 31-46 🏁 Script executed: #!/bin/bash
# Show implementation of verifyAccessToken with surrounding context
rg -nC5 "verifyAccessToken" packages/mcp-server/src/auth.ts
# Show file imports to verify how fetch is being provided
sed -n '1,30p' packages/mcp-server/src/auth.ts Length of output: 2035 🌐 Web query:
💡 Result: Short answer
Recommended way to add a timeout
Sources 🏁 Script executed: #!/bin/bash
rg -nC3 "scopes" packages/mcp-server/src/auth.ts Length of output: 305 Add request timeout, remove mismatched Content-Type header, and clear timer in verifyAccessToken
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log('Token validation response status:', response.status); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new Error(`invalid_token (status ${response.status})`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const tokenData: any = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
token, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
clientId: tokenData.clientInfo?.name || 'unknown', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
scopes: tokenData.scopes || ['read'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Token validation failed:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+19
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add network timeouts to external fetch calls. Avoids hanging on network stalls. Apply: - const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/v0/mcp/validate`, {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 10_000);
+ const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/v0/mcp/validate`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
- });
+ signal: controller.signal,
+ });
+ clearTimeout(timeout); 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
getClient: async (client_id: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
redirect_uris: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
redirect_uris ? redirect_uris : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BEEPER_MCP_BASE_URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'http://localhost:6274/oauth/callback/debug', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'http://localhost:6274/oauth/callback', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const createMCPAuthRouter = (redirect_uris?: string[]): express.RequestHandler => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const proxyProvider = createProxyProvider(redirect_uris); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return mcpAuthRouter({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
provider: proxyProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
issuerUrl: new URL(BEEPER_DESKTOP_BASE_URL), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
baseUrl: new URL(BEEPER_MCP_BASE_URL), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const customWellKnownEndpoint = (req: express.Request, res: express.Response) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
res.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
resource: BEEPER_MCP_BASE_URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
authorization_servers: [BEEPER_DESKTOP_BASE_URL], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const sendUnauthorizedResponse = (res: express.Response, error?: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const wwwAuth = `Bearer resource_metadata="${BEEPER_MCP_BASE_URL}/.well-known/oauth-protected-resource"`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
res.set('WWW-Authenticate', wwwAuth); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
res.status(401).json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
jsonrpc: '2.0', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
error: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
code: -32000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
message: `Invalid request: ${fromError(error)}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const getTokenForStdio = async (): Promise<string> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (BEEPER_AUTH_TOKEN) return BEEPER_AUTH_TOKEN; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Needs to be implemented | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/oauth/token`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
method: 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'Content-Type': 'application/x-www-form-urlencoded', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
body: new URLSearchParams({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
grant_type: 'client_credentials', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
client_id: 'stdio-mcp-client', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// client_secret: process.env.MCP_CLIENT_SECRET || '', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
scope: 'read write', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new Error(`Failed to get token: ${response.status}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const data = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return (data as any).access_token; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Harden token acquisition; add timeout and clearer error messages. Prevents hangs and improves diagnostics. export const getTokenForStdio = async (): Promise<string> => {
if (BEEPER_AUTH_TOKEN) return BEEPER_AUTH_TOKEN;
// Needs to be implemented
- const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/oauth/token`, {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 10_000);
+ const response = await fetch(`${BEEPER_DESKTOP_BASE_URL}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': 'application/json',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
- client_id: 'stdio-mcp-client',
+ client_id: readEnv('MCP_CLIENT_ID') ?? 'stdio-mcp-client',
// client_secret: process.env.MCP_CLIENT_SECRET || '',
scope: 'read write',
}),
- });
+ signal: controller.signal,
+ }).catch((e) => {
+ throw new Error(`Token request failed: ${e?.name === 'AbortError' ? 'timeout' : String(e)}`);
+ });
+ clearTimeout(timeout);
if (!response.ok) {
- throw new Error(`Failed to get token: ${response.status}`);
+ let details = '';
+ try { details = JSON.stringify(await response.json()); } catch {}
+ throw new Error(`Failed to get token: HTTP ${response.status}${details ? ` ${details}` : ''}`);
}
const data = await response.json();
return (data as any).access_token;
}; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,20 @@ | ||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. | ||
|
||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; | ||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; | ||
|
||
import cors from 'cors'; | ||
import express from 'express'; | ||
import { fromError } from 'zod-validation-error/v3'; | ||
import { McpOptions, parseQueryOptions } from './options'; | ||
import { initMcpServer, newMcpServer } from './server'; | ||
import { parseAuthHeaders } from './headers'; | ||
|
||
const oauthResourceIdentifier = (req: express.Request): string => { | ||
const protocol = req.headers['x-forwarded-proto'] ?? req.protocol; | ||
return `${protocol}://${req.get('host')}/`; | ||
}; | ||
import { createMCPAuthRouter, customWellKnownEndpoint, sendUnauthorizedResponse } from './auth'; | ||
|
||
const newServer = ( | ||
defaultMcpOptions: McpOptions, | ||
req: express.Request, | ||
res: express.Response, | ||
): McpServer | null => { | ||
const server = newMcpServer(); | ||
|
||
let mcpOptions: McpOptions; | ||
|
||
try { | ||
mcpOptions = parseQueryOptions(defaultMcpOptions, req.query); | ||
} catch (error) { | ||
|
@@ -38,6 +30,12 @@ const newServer = ( | |
|
||
try { | ||
const authOptions = parseAuthHeaders(req); | ||
|
||
if (!authOptions.accessToken) { | ||
sendUnauthorizedResponse(res); | ||
return null; | ||
} | ||
|
||
Comment on lines
31
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Validate the bearer token before initializing the MCP server Presence-only check accepts any token string. Validate it and propagate the specific error in the 401 challenge. - const authOptions = parseAuthHeaders(req);
-
- if (!authOptions.accessToken) {
- sendUnauthorizedResponse(res);
- return null;
- }
+ const authOptions = parseAuthHeaders(req);
+ if (!authOptions.accessToken) {
+ sendUnauthorizedResponse(req, res);
+ return null;
+ }
+ try {
+ await validateAccessToken(authOptions.accessToken);
+ } catch (error) {
+ sendUnauthorizedResponse(req, res, error);
+ return null;
+ } Note: import validateAccessToken and updated sendUnauthorizedResponse. -import { createMCPAuthRouter, customWellKnownEndpoint, sendUnauthorizedResponse } from './auth';
+import {
+ createMCPAuthRouter,
+ customWellKnownEndpoint,
+ sendUnauthorizedResponse,
+ validateAccessToken,
+} from './auth'; 🤖 Prompt for AI Agents
|
||
initMcpServer({ | ||
server: server, | ||
clientOptions: { | ||
|
@@ -48,19 +46,8 @@ const newServer = ( | |
}, | ||
mcpOptions, | ||
}); | ||
} catch { | ||
const resourceIdentifier = oauthResourceIdentifier(req); | ||
res.set( | ||
'WWW-Authenticate', | ||
`Bearer resource_metadata="${resourceIdentifier}.well-known/oauth-protected-resource"`, | ||
); | ||
res.status(401).json({ | ||
jsonrpc: '2.0', | ||
error: { | ||
code: -32000, | ||
message: 'Unauthorized', | ||
}, | ||
}); | ||
} catch (error) { | ||
sendUnauthorizedResponse(res, error); | ||
return null; | ||
} | ||
|
||
|
@@ -69,12 +56,14 @@ const newServer = ( | |
|
||
const post = (defaultOptions: McpOptions) => async (req: express.Request, res: express.Response) => { | ||
const server = newServer(defaultOptions, req, res); | ||
// If we return null, we already set the authorization error. | ||
|
||
if (server === null) return; | ||
|
||
const transport = new StreamableHTTPServerTransport({ | ||
// Stateless server | ||
sessionIdGenerator: undefined, | ||
}); | ||
|
||
await server.connect(transport); | ||
await transport.handleRequest(req, res, req.body); | ||
}; | ||
|
@@ -99,22 +88,16 @@ const del = async (req: express.Request, res: express.Response) => { | |
}); | ||
}; | ||
|
||
const oauthMetadata = (req: express.Request, res: express.Response) => { | ||
const resourceIdentifier = oauthResourceIdentifier(req); | ||
res.json({ | ||
resource: resourceIdentifier, | ||
authorization_servers: ['http://localhost:23373/oauth/authorize'], | ||
bearer_methods_supported: ['header'], | ||
scopes_supported: 'read write', | ||
}); | ||
}; | ||
|
||
export const streamableHTTPApp = (options: McpOptions): express.Express => { | ||
const app = express(); | ||
|
||
app.set('query parser', 'extended'); | ||
app.use(express.json()); | ||
|
||
app.get('/.well-known/oauth-protected-resource', cors(), oauthMetadata); | ||
const beeperProxyRouter = createMCPAuthRouter(); | ||
app.get('/.well-known/oauth-protected-resource', (req, res) => customWellKnownEndpoint(req, res)); | ||
app.use(beeperProxyRouter); | ||
|
||
app.get('/', get); | ||
app.post('/', post(options)); | ||
app.delete('/', del); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -7,6 +7,7 @@ import { | |||||||||||||||
CallToolRequestSchema, | ||||||||||||||||
Implementation, | ||||||||||||||||
ListToolsRequestSchema, | ||||||||||||||||
SetLevelRequestSchema, | ||||||||||||||||
Tool, | ||||||||||||||||
} from '@modelcontextprotocol/sdk/types.js'; | ||||||||||||||||
Comment on lines
+10
to
12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Also import LoggingLevel to validate and handle the full set of MCP log levels. You’re adding a SetLevel handler; pull in LoggingLevel so we can type-check and support all levels (debug, info, notice, warning, error, critical, alert, emergency). CallToolRequestSchema,
Implementation,
ListToolsRequestSchema,
- SetLevelRequestSchema,
+ SetLevelRequestSchema,
+ LoggingLevel,
Tool,
} from '@modelcontextprotocol/sdk/types.js'; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||
import { ClientOptions } from '@beeper/desktop-api'; | ||||||||||||||||
|
@@ -91,6 +92,7 @@ export function initMcpServer(params: { | |||||||||||||||
|
||||||||||||||||
const client = new BeeperDesktop({ | ||||||||||||||||
logger, | ||||||||||||||||
skipAccessToken: true, | ||||||||||||||||
...params.clientOptions, | ||||||||||||||||
defaultHeaders: { | ||||||||||||||||
...params.clientOptions?.defaultHeaders, | ||||||||||||||||
|
@@ -119,6 +121,12 @@ export function initMcpServer(params: { | |||||||||||||||
|
||||||||||||||||
return executeHandler(endpoint.tool, endpoint.handler, client, args, mcpOptions.capabilities); | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
server.setRequestHandler(SetLevelRequestSchema, async (request) => { | ||||||||||||||||
const { level } = request.params; | ||||||||||||||||
logger.info(`Log level set to: ${level}`); | ||||||||||||||||
return {}; | ||||||||||||||||
}); | ||||||||||||||||
Comment on lines
+125
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SetLevel handler is a no-op; it should update the server’s minimum log level or defer to the SDK’s default. At present it only logs; it doesn’t change behavior. Implement level validation and update the gate. Per spec, reject invalid params with an error. (spec.modelcontextprotocol.io) - server.setRequestHandler(SetLevelRequestSchema, async (request) => {
- const { level } = request.params;
- logger.info(`Log level set to: ${level}`);
- return {};
- });
+ server.setRequestHandler(SetLevelRequestSchema, async (request) => {
+ const { level } = request.params;
+ const valid: LoggingLevel[] = [
+ 'debug','info','notice','warning','error','critical','alert','emergency'
+ ];
+ if (!valid.includes(level)) {
+ throw new Error(`Invalid params: unsupported level "${level}"`);
+ }
+ currentLevel = level;
+ // Optionally acknowledge at info (will be gated appropriately)
+ logger.info(`Log level set to: ${level}`);
+ return {};
+ });
|
||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,28 @@ | ||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | ||
import { initMcpServer, newMcpServer } from './server'; | ||
import { McpOptions } from './options'; | ||
import { getTokenForStdio } from './auth'; | ||
|
||
export const launchStdioServer = async (options: McpOptions) => { | ||
const server = newMcpServer(); | ||
try { | ||
const token = await getTokenForStdio(); | ||
const server = newMcpServer(); | ||
|
||
initMcpServer({ server, mcpOptions: options }); | ||
initMcpServer({ | ||
server, | ||
clientOptions: { | ||
defaultHeaders: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}, | ||
mcpOptions: options, | ||
}); | ||
batuhan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const transport = new StdioServerTransport(); | ||
await server.connect(transport); | ||
console.error('MCP Server running on stdio'); | ||
const transport = new StdioServerTransport(); | ||
await server.connect(transport); | ||
console.error('MCP Server running on stdio'); | ||
} catch (error) { | ||
console.error('Failed to obtain access token:', error); | ||
process.exit(1); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { CustomHandlerFunction, asFormattedMCPContentResult } from '../types'; | ||
|
||
const CONTACT_SUPPORT = | ||
'Something unexpected happened. User might need to contact support at mailto:[email protected]?subject=Something%20wrong%20in%20the%20Beeper%20Desktop%20API'; | ||
|
||
export const getAccountsHandler: CustomHandlerFunction = async (client, args) => { | ||
const output = await client.accounts.list(); | ||
|
||
if (!output || output.length === 0) { | ||
return asFormattedMCPContentResult(`No accounts found. ${CONTACT_SUPPORT}`); | ||
} | ||
|
||
const lines: string[] = []; | ||
lines.push('# Accounts'); | ||
for (const acc of output) { | ||
if (!acc.user) { | ||
lines.push(`\n## ${acc.network}`); | ||
lines.push(`**Account ID**: \`${acc.accountID}\``); | ||
lines.push('**User**: Unknown'); | ||
continue; | ||
} | ||
|
||
const name = acc.user.fullName || acc.user.username || acc.user.id; | ||
lines.push(`\n## ${acc.network}`); | ||
lines.push(`**Account ID**: \`${acc.accountID}\``); | ||
lines.push(`**User**: ${name}`); | ||
if (acc.user.email) lines.push(`**Email**: ${acc.user.email}`); | ||
if (acc.user.phoneNumber) lines.push(`**Phone**: ${acc.user.phoneNumber}`); | ||
} | ||
lines.push('\n# Using this information\n'); | ||
lines.push('- Pass accountIDs to narrow chat/message queries when known.'); | ||
|
||
return asFormattedMCPContentResult(lines.join('\n')); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { CustomHandlerFunction, asFormattedMCPContentResult } from '../types'; | ||
|
||
export const openInAppHandler: CustomHandlerFunction = async (client, args) => { | ||
const currArgs = args as any; | ||
const output = await client.app.open(currArgs); | ||
|
||
const lines: string[] = []; | ||
lines.push('# App'); | ||
if (output.success) { | ||
lines.push('Beeper was opened.'); | ||
if (currArgs?.chatID) { | ||
const chatRef = String(currArgs.chatID); | ||
lines.push(`Focused chat: ${chatRef}`); | ||
} | ||
if (currArgs?.draftText) { | ||
lines.push('Draft text populated.'); | ||
} | ||
} else { | ||
lines.push('Failed to open Beeper.'); | ||
} | ||
lines.push('\n# Using this information\n'); | ||
lines.push('- Use search_chats or get_chat to retrieve chat context.'); | ||
|
||
return asFormattedMCPContentResult(lines.join('\n')); | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,18 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { CustomHandlerFunction, asFormattedMCPContentResult } from '../types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const archiveChatHandler: CustomHandlerFunction = async (client, args) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const currArgs = args as any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const output = await client.chats.archive(currArgs); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const lines: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('# Archive Chat'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (output.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push(`Chat ${currArgs?.chatID} ${currArgs?.archived === false ? 'unarchived' : 'archived'}.`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('Failed to update archive state.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('\n# Using this information\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('- Use search_chats to verify the chat moved to the expected inbox.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return asFormattedMCPContentResult(lines.join('\n')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+3
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Validate args and catch client errors; avoid leading newline in headings Type args, guard chatID, handle exceptions, and remove the embedded newline heading. import { CustomHandlerFunction, asFormattedMCPContentResult } from '../types';
+type ArchiveChatArgs = { chatID: string | number; archived?: boolean };
+
export const archiveChatHandler: CustomHandlerFunction = async (client, args) => {
- const currArgs = args as any;
- const output = await client.chats.archive(currArgs);
-
- const lines: string[] = [];
- lines.push('# Archive Chat');
- if (output.success) {
- lines.push(`Chat ${currArgs?.chatID} ${currArgs?.archived === false ? 'unarchived' : 'archived'}.`);
- } else {
- lines.push('Failed to update archive state.');
- }
- lines.push('\n# Using this information\n');
- lines.push('- Use search_chats to verify the chat moved to the expected inbox.');
-
- return asFormattedMCPContentResult(lines.join('\n'));
+ const currArgs = args as ArchiveChatArgs;
+ if (!currArgs?.chatID) {
+ return asFormattedMCPContentResult('Failed to update archive state: chatID is required.');
+ }
+ try {
+ const output = await client.chats.archive(currArgs);
+ const lines: string[] = [];
+ lines.push('# Archive Chat');
+ if (output.success) {
+ const action = currArgs?.archived === false ? 'unarchived' : 'archived';
+ lines.push(`Chat ${currArgs.chatID} ${action}.`);
+ } else {
+ lines.push('Failed to update archive state.');
+ }
+ lines.push('');
+ lines.push('# Using this information');
+ lines.push('- Use search_chats to verify the chat moved to the expected inbox.');
+ return asFormattedMCPContentResult(lines.join('\n'));
+ } catch (err: any) {
+ return asFormattedMCPContentResult(
+ `Failed to update archive state: ${err?.message || String(err)}`,
+ );
+ }
}; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,23 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { CustomHandlerFunction, asFormattedMCPContentResult } from '../types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { formatChatToMarkdown } from '../utils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const getChatHandler: CustomHandlerFunction = async (client, args) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const currArgs = args as any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const chat = await client.chats.retrieve(currArgs); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const lines: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!chat) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('# Chat'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('Not found.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return asFormattedMCPContentResult(lines.join('\n')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (const line of formatChatToMarkdown(chat, client.baseURL)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push(line); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('\n# Using this information\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('- Use search_messages to find specific content in this chat.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lines.push('- Link the "open" link to the user to allow them to view the chat in Beeper Desktop.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return asFormattedMCPContentResult(lines.join('\n')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+4
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Handle exceptions and surface isError on unexpected failures Wrap retrieval in try/catch; 404-style “Not found” can remain non-error. -export const getChatHandler: CustomHandlerFunction = async (client, args) => {
- const currArgs = args as any;
- const chat = await client.chats.retrieve(currArgs);
-
- const lines: string[] = [];
- if (!chat) {
- lines.push('# Chat');
- lines.push('Not found.');
- return asFormattedMCPContentResult(lines.join('\n'));
- }
-
- for (const line of formatChatToMarkdown(chat, client.baseURL)) {
- lines.push(line);
- }
- lines.push('\n# Using this information\n');
- lines.push('- Use search_messages to find specific content in this chat.');
- lines.push('- Link the "open" link to the user to allow them to view the chat in Beeper Desktop.');
-
- return asFormattedMCPContentResult(lines.join('\n'));
-};
+export const getChatHandler: CustomHandlerFunction = async (client, args) => {
+ const currArgs = args as any;
+ const lines: string[] = [];
+ try {
+ const chat = await client.chats.retrieve(currArgs);
+ if (!chat) {
+ lines.push('# Chat');
+ lines.push('Not found.');
+ return asFormattedMCPContentResult(lines.join('\n'));
+ }
+ for (const line of formatChatToMarkdown(chat, client.baseURL)) {
+ lines.push(line);
+ }
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ lines.push('# Chat');
+ lines.push(`Failed to retrieve chat: ${msg}.`);
+ return asFormattedMCPContentResult(lines.join('\n'), { isError: true });
+ }
+ lines.push('\n# Using this information\n');
+ lines.push('- Use search_messages to find specific content in this chat.');
+ lines.push('- Link the "open" link to the user to allow them to view the chat in Beeper Desktop.');
+ return asFormattedMCPContentResult(lines.join('\n'));
+}; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Env var name mismatch for auth token; read both to avoid silent fallback.
Constant is BEEPER_AUTH_TOKEN, but you read AUTH_TOKEN. This will ignore BEEPER_AUTH_TOKEN if set.
Apply:
📝 Committable suggestion
🤖 Prompt for AI Agents