Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"build": "tsc && shx cp setup-claude-server.js uninstall-claude-server.js track-installation.js dist/ && shx chmod +x dist/*.js && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && shx mkdir -p dist/remote-device/scripts && shx cp src/remote-device/scripts/blocking-offline-update.js dist/remote-device/scripts/ && node scripts/build-ui-runtime.cjs",
"watch": "tsc --watch",
"start": "node dist/index.js",
"start:http": "node dist/index.js http",
"start:debug": "node --inspect-brk=9229 dist/index.js",
"setup": "npm install --include=dev && npm run build && node setup-claude-server.js",
"setup:debug": "npm install && npm run build && node setup-claude-server.js --debug",
Expand Down
311 changes: 311 additions & 0 deletions src/http-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
/**
* Streamable HTTP transport for DesktopCommanderMCP with OAuth 2.1 support.
*
* Allows Claude Web (claude.ai) to connect to this MCP server over HTTP.
* Intended to sit behind an nginx reverse proxy that handles TLS termination.
*
* OAuth flow (handled automatically by Claude Web):
* 1. Client POSTs to /mcp, gets 401
* 2. Client discovers metadata at /.well-known/oauth-protected-resource/mcp
* 3. Client gets auth server metadata at /.well-known/oauth-authorization-server
* 4. Client registers via DCR at /register (or uses pre-configured client_id/secret)
* 5. Client redirects user to /authorize
* 6. Server auto-approves and redirects back with auth code
* 7. Client exchanges code for token at /token
* 8. Client uses Bearer token for /mcp requests
*
* Usage:
* node dist/index.js http
* PUBLIC_URL=https://serea.xyz OAUTH_CLIENT_ID=... OAUTH_CLIENT_SECRET=... node dist/index.js http
*/

import { randomUUID } from 'node:crypto';
import { createServer as createHttpServer } from 'node:http';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import type { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/sdk/server/auth/provider.js';
import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import type { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
// @ts-ignore - express types not installed
import type { Response } from 'express';
import { createServer } from './server.js';
import { configManager } from './config-manager.js';
import { featureFlagManager } from './utils/feature-flags.js';

const PORT = parseInt(process.env.PORT || '3100', 10);
const HOST = '127.0.0.1';
const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`;
const OAUTH_CLIENT_ID = process.env.OAUTH_CLIENT_ID!;
const OAUTH_CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET!;

if (!OAUTH_CLIENT_ID || !OAUTH_CLIENT_SECRET) {
console.error('[http] OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET env vars are required.');
process.exit(1);
}

// ---------------------------------------------------------------------------
// Pre-registered client store (no DCR — only known client_id can connect)
// ---------------------------------------------------------------------------

class PreRegisteredClientsStore implements OAuthRegisteredClientsStore {
private client: OAuthClientInformationFull;

constructor(clientId: string, clientSecret: string) {
this.client = {
client_id: clientId,
client_secret: clientSecret,
client_id_issued_at: Math.floor(Date.now() / 1000),
redirect_uris: [
'https://claude.ai/api/mcp/auth_callback',
'https://claude.com/api/mcp/auth_callback',
],
grant_types: ['authorization_code', 'refresh_token'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The client metadata advertises support for the "refresh_token" grant type even though the OAuth provider explicitly throws for refresh token exchanges, so clients that rely on the metadata may legitimately attempt a refresh and encounter unexpected failures or 5xx errors instead of a supported flow. [logic error]

Severity Level: Major ⚠️
- ❌ OAuth refresh_token grant fails despite advertised support.
- ⚠️ Remote MCP clients cannot refresh access tokens.
- ⚠️ Users may see unexpected token-refresh errors.
Suggested change
grant_types: ['authorization_code', 'refresh_token'],
grant_types: ['authorization_code'],
Steps of Reproduction ✅
1. Start the HTTP MCP server via `node dist/index.js http` so that `main()` in
`src/http-server.ts:179-305` registers OAuth routes using `mcpAuthRouter(...)` (lines
~196-206).

2. As an OAuth client, fetch authorization server metadata from `GET
${PUBLIC_URL}/.well-known/oauth-authorization-server`, whose URL is logged in
`src/http-server.ts:285` and served by `mcpAuthRouter`.

3. Observe that the pre-registered client configuration created in
`PreRegisteredClientsStore` at `src/http-server.ts:57-70` includes `grant_types:
['authorization_code', 'refresh_token']`, advertising refresh token support.

4. After performing an authorization code flow (handled by
`AutoApproveOAuthProvider.exchangeAuthorizationCode()` at `src/http-server.ts:114-139`),
attempt to refresh using `POST ${PUBLIC_URL}/token` with `grant_type=refresh_token`;
`mcpAuthRouter` calls `AutoApproveOAuthProvider.exchangeRefreshToken()` at
`src/http-server.ts:141-147`, which throws `Error('Refresh tokens not supported')`,
resulting in a failed refresh despite the advertised grant type.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/http-server.ts
**Line:** 66:66
**Comment:**
	*Logic Error: The client metadata advertises support for the "refresh_token" grant type even though the OAuth provider explicitly throws for refresh token exchanges, so clients that rely on the metadata may legitimately attempt a refresh and encounter unexpected failures or 5xx errors instead of a supported flow.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

response_types: ['code'],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
token_endpoint_auth_method: 'client_secret_post',
client_name: 'Claude Web',
} as OAuthClientInformationFull;
}

getClient(clientId: string): OAuthClientInformationFull | undefined {
if (clientId === this.client.client_id) {
return this.client;
}
return undefined;
}

// No registerClient method = DCR disabled
}

class AutoApproveOAuthProvider implements OAuthServerProvider {
readonly clientsStore: PreRegisteredClientsStore;
private codes = new Map<string, { client: OAuthClientInformationFull; params: AuthorizationParams }>();
private tokens = new Map<string, { token: string; clientId: string; scopes: string[]; expiresAt: number; resource?: URL }>();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

constructor(clientId: string, clientSecret: string) {
this.clientsStore = new PreRegisteredClientsStore(clientId, clientSecret);
}

async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
const code = randomUUID();
this.codes.set(code, { client, params });

const searchParams = new URLSearchParams({ code });
if (params.state !== undefined) {
searchParams.set('state', params.state);
}

// Auto-approve: redirect immediately with auth code
const targetUrl = new URL(params.redirectUri);
targetUrl.search = searchParams.toString();
console.log(`[oauth] Auto-approved authorization for client ${client.client_id}`);
res.redirect(targetUrl.toString());
}

async challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string> {
const codeData = this.codes.get(authorizationCode);
if (!codeData) throw new Error('Invalid authorization code');
return codeData.params.codeChallenge;
}

async exchangeAuthorizationCode(
client: OAuthClientInformationFull,
authorizationCode: string,
_codeVerifier?: string,
_redirectUri?: string,
_resource?: URL,
): Promise<OAuthTokens> {
const codeData = this.codes.get(authorizationCode);
if (!codeData) throw new Error('Invalid authorization code');
if (codeData.client.client_id !== client.client_id) {
throw new Error('Authorization code was not issued to this client');
}

this.codes.delete(authorizationCode);

const token = randomUUID();
this.tokens.set(token, {
token,
clientId: client.client_id,
scopes: codeData.params.scopes || [],
expiresAt: Date.now() + 3600000, // 1 hour
resource: codeData.params.resource,
});

console.log(`[oauth] Issued access token for client ${client.client_id}`);
return {
access_token: token,
token_type: 'bearer',
expires_in: 3600,
scope: (codeData.params.scopes || []).join(' '),
};
}
Comment on lines +114 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PKCE code verifier is not validated.

The exchangeAuthorizationCode method accepts _codeVerifier but doesn't verify it against the stored codeChallenge. PKCE (Proof Key for Code Exchange) requires validating that SHA256(codeVerifier) === codeChallenge to prevent authorization code interception attacks.

🔒 Proposed fix to validate PKCE code verifier
+import { createHash } from 'node:crypto';
+
+function verifyPkce(codeVerifier: string, codeChallenge: string): boolean {
+  const hash = createHash('sha256').update(codeVerifier).digest('base64url');
+  return hash === codeChallenge;
+}
+
 async exchangeAuthorizationCode(
   client: OAuthClientInformationFull,
   authorizationCode: string,
-  _codeVerifier?: string,
+  codeVerifier?: string,
   _redirectUri?: string,
   _resource?: URL,
 ): Promise<OAuthTokens> {
   const codeData = this.codes.get(authorizationCode);
   if (!codeData) throw new Error('Invalid authorization code');
   if (codeData.client.client_id !== client.client_id) {
     throw new Error('Authorization code was not issued to this client');
   }
+  if (codeData.params.codeChallenge && codeVerifier) {
+    if (!verifyPkce(codeVerifier, codeData.params.codeChallenge)) {
+      throw new Error('Invalid code verifier');
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/http-server.ts` around lines 114 - 145, The exchangeAuthorizationCode
method currently ignores the provided _codeVerifier; update it to enforce PKCE:
when the retrieved codeData.params contains a codeChallenge (and optionally
codeChallengeMethod), require a non-empty _codeVerifier and validate it by
computing SHA-256 of the provided _codeVerifier, base64url-encoding the digest,
and comparing that value to the stored codeChallenge (supporting
codeChallengeMethod 'S256' only); if they don't match (or verifier missing when
a challenge exists) throw an Error like 'Invalid PKCE code verifier'. Implement
this logic inside exchangeAuthorizationCode before deleting the code from
this.codes so you reference codeData.params.codeChallenge /
codeData.params.codeChallengeMethod and the _codeVerifier argument.


async exchangeRefreshToken(
_client: OAuthClientInformationFull,
Comment on lines +136 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Access tokens are stored indefinitely in the in-memory tokens map and are never removed when expired, causing unbounded growth over time and a memory leak under repeated authentications. [resource leak]

Severity Level: Major ⚠️
- ⚠️ OAuth provider memory usage grows with each token issued.
- ⚠️ Long-lived servers may accumulate many expired token entries.
Suggested change
});
console.log(`[oauth] Issued access token for client ${client.client_id}`);
return {
access_token: token,
token_type: 'bearer',
expires_in: 3600,
scope: (codeData.params.scopes || []).join(' '),
};
}
async exchangeRefreshToken(
_client: OAuthClientInformationFull,
async verifyAccessToken(token: string): Promise<AuthInfo> {
const tokenData = this.tokens.get(token);
if (!tokenData) {
throw new Error('Invalid or expired token');
}
if (tokenData.expiresAt < Date.now()) {
this.tokens.delete(token);
throw new Error('Invalid or expired token');
}
return {
token,
clientId: tokenData.clientId,
scopes: tokenData.scopes,
expiresAt: Math.floor(tokenData.expiresAt / 1000),
resource: tokenData.resource,
};
}
Steps of Reproduction ✅
1. Start the HTTP MCP server via `main()` in `src/http-server.ts:157-167`; this creates an
`AutoApproveOAuthProvider` instance at line 167 with an internal `tokens` map defined at
line 77.

2. Complete the OAuth authorization code flow at least once via the `/authorize` and
`/token` routes wired by `mcpAuthRouter` at `src/http-server.ts:173-179`; the `/token`
handler calls `AutoApproveOAuthProvider.exchangeAuthorizationCode()` at lines 99-127.

3. Inside `exchangeAuthorizationCode()`, a new access token is generated and stored in
`this.tokens` at `src/http-server.ts:112-119`, increasing `this.tokens.size` by one for
each completed flow.

4. Repeat the auth flow many times over the lifetime of the process; expired tokens (older
than one hour per `expiresAt` set at line 117) are never removed anywhere in the file,
since `verifyAccessToken()` at `src/http-server.ts:136-148` only checks `expiresAt <
Date.now()` and throws but does not delete entries. Observing `this.tokens.size` via
instrumentation or a debugger shows it monotonically increasing, indicating unbounded
in-memory growth tied to the number of issued tokens.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/http-server.ts
**Line:** 136:148
**Comment:**
	*Resource Leak: Access tokens are stored indefinitely in the in-memory `tokens` map and are never removed when expired, causing unbounded growth over time and a memory leak under repeated authentications.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

_refreshToken: string,
_scopes?: string[],
_resource?: URL,
): Promise<OAuthTokens> {
throw new Error('Refresh tokens not supported');
}

async verifyAccessToken(token: string): Promise<AuthInfo> {
const tokenData = this.tokens.get(token);
if (!tokenData || tokenData.expiresAt < Date.now()) {
throw new Error('Invalid or expired token');
}
return {
token,
Comment on lines +150 to +169

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Access tokens are stored in an in-memory Map and never removed when they expire or are rejected, causing unbounded growth of the token store over time in a long-running process and risking memory exhaustion. [resource leak]

Severity Level: Major ⚠️
- ⚠️ In-memory token Map grows with every authorization flow.
- ⚠️ Long-running HTTP MCP server risks gradual memory exhaustion.
- ⚠️ Increased GC pressure may degrade MCP request latency.
Suggested change
_scopes?: string[],
_resource?: URL,
): Promise<OAuthTokens> {
throw new Error('Refresh tokens not supported');
}
async verifyAccessToken(token: string): Promise<AuthInfo> {
const tokenData = this.tokens.get(token);
if (!tokenData || tokenData.expiresAt < Date.now()) {
throw new Error('Invalid or expired token');
}
return {
token,
async verifyAccessToken(token: string): Promise<AuthInfo> {
const tokenData = this.tokens.get(token);
if (!tokenData) {
throw new Error('Invalid or expired token');
}
if (tokenData.expiresAt < Date.now()) {
this.tokens.delete(token);
throw new Error('Invalid or expired token');
}
return {
token,
clientId: tokenData.clientId,
scopes: tokenData.scopes,
expiresAt: Math.floor(tokenData.expiresAt / 1000),
resource: tokenData.resource,
};
}
Steps of Reproduction ✅
1. Start the HTTP MCP server via `node dist/index.js http` so that `main()` in
`src/http-server.ts:179-305` initializes `AutoApproveOAuthProvider` and its in-memory
`tokens` Map (defined at `src/http-server.ts:83-87`).

2. From a client, perform the OAuth authorization code flow multiple times, causing
`AutoApproveOAuthProvider.exchangeAuthorizationCode()` at `src/http-server.ts:114-139` to
execute and call `this.tokens.set(token, {...})` for each new access token.

3. Keep the server running for hours or days; no code anywhere in the repository calls
`this.tokens.delete(...)` on these entries (the only references are `set` at line 124 and
`get` at line 151), so expired tokens remain stored indefinitely.

4. When an expired token is presented on any `/mcp` request (routes at
`src/http-server.ts:216-271` protected by `authMiddleware` created at lines 209-213),
`requireBearerAuth` invokes `provider.verifyAccessToken()` at
`src/http-server.ts:150-162`, which throws for expired tokens but never removes them from
`this.tokens`, allowing unbounded growth visible in heap snapshots or instrumentation.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/http-server.ts
**Line:** 150:162
**Comment:**
	*Resource Leak: Access tokens are stored in an in-memory Map and never removed when they expire or are rejected, causing unbounded growth of the token store over time in a long-running process and risking memory exhaustion.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

clientId: tokenData.clientId,
scopes: tokenData.scopes,
expiresAt: Math.floor(tokenData.expiresAt / 1000),
resource: tokenData.resource,
};
}
}

// ---------------------------------------------------------------------------
// Active MCP transports keyed by session ID
// ---------------------------------------------------------------------------
const sessions = new Map<string, StreamableHTTPServerTransport>();

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
// Load configuration
try {
await configManager.loadConfig();
await featureFlagManager.initialize();
} catch (err) {
console.error('[http] Failed to load config, continuing with defaults:', err);
}

const issuerUrl = new URL(PUBLIC_URL);
const mcpServerUrl = new URL('/mcp', PUBLIC_URL);

const provider = new AutoApproveOAuthProvider(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET);

const publicHostname = new URL(PUBLIC_URL).hostname;
const app = createMcpExpressApp({ host: HOST, allowedHosts: [publicHostname, HOST, 'localhost'] });

// Trust the nginx reverse proxy (fixes express-rate-limit X-Forwarded-For error)
app.set('trust proxy', 1);

// ---------- OAuth routes (authorize, token, register, metadata) ----------
app.use(mcpAuthRouter({
provider,
issuerUrl,
resourceServerUrl: mcpServerUrl,
scopesSupported: ['mcp:tools'],
resourceName: 'DesktopCommanderMCP',
}));

// ---------- Bearer auth middleware for /mcp ----------
const authMiddleware = requireBearerAuth({
verifier: provider,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
Comment on lines +211 to +219

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce the advertised scope on /mcp bearer auth.

Line 211 advertises mcp:tools, but Line 218 accepts any token (requiredScopes: []). This weakens authorization checks.

🔒 Suggested fix
   const authMiddleware = requireBearerAuth({
     verifier: provider,
-    requiredScopes: [],
+    requiredScopes: ['mcp:tools'],
     resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scopesSupported: ['mcp:tools'],
resourceName: 'DesktopCommanderMCP',
}));
// ---------- Bearer auth middleware for /mcp ----------
const authMiddleware = requireBearerAuth({
verifier: provider,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
scopesSupported: ['mcp:tools'],
resourceName: 'DesktopCommanderMCP',
}));
// ---------- Bearer auth middleware for /mcp ----------
const authMiddleware = requireBearerAuth({
verifier: provider,
requiredScopes: ['mcp:tools'],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/http-server.ts` around lines 211 - 219, The bearer auth is advertising
mcp:tools via resourceName/metadata but auth middleware uses requiredScopes: []
so tokens with no scope pass; update the requireBearerAuth call (authMiddleware)
to enforce the advertised scope by setting requiredScopes: ['mcp:tools'] (or
derive it from the resource metadata/resourceName) while keeping verifier:
provider and resourceMetadataUrl:
getOAuthProtectedResourceMetadataUrl(mcpServerUrl) unchanged so /mcp actually
requires the mcp:tools scope.

});

// ---------- POST /mcp ----------
app.post('/mcp', authMiddleware, async (req: any, res: any) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

if (sessionId && sessions.has(sessionId)) {
transport = sessions.get(sessionId)!;
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
console.log(`[http] Session initialized: ${sid}`);
sessions.set(sid, transport);
},
});

const server = createServer();

transport.onclose = () => {
const sid = transport.sessionId;
if (sid) {
console.log(`[http] Session closed: ${sid}`);
sessions.delete(sid);
}
transport.close().catch(() => {});
server.close().catch(() => {});
};

await server.connect(transport);
} else {
res.status(400).json({ error: 'Bad Request: no valid session. Send an initialize request first.' });
return;
}

await transport.handleRequest(req, res, req.body);
});

// ---------- GET /mcp (SSE) ----------
app.get('/mcp', authMiddleware, async (req: any, res: any) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !sessions.has(sessionId)) {
res.status(400).json({ error: 'Bad Request: invalid or missing session ID.' });
return;
}
const transport = sessions.get(sessionId)!;
await transport.handleRequest(req, res);
});

// ---------- DELETE /mcp ----------
app.delete('/mcp', authMiddleware, async (req: any, res: any) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !sessions.has(sessionId)) {
res.status(400).json({ error: 'Bad Request: invalid or missing session ID.' });
return;
}
const transport = sessions.get(sessionId)!;
await transport.handleRequest(req, res);
});

// ---------- Health check ----------
app.get('/health', (_req: any, res: any) => {
res.json({ status: 'ok', sessions: sessions.size });
});

// ---------- Start listening ----------
const httpServer = createHttpServer(app);
httpServer.listen(PORT, HOST, () => {
console.log(`[http] MCP server listening on http://${HOST}:${PORT}`);
console.log(`[http] Public URL: ${PUBLIC_URL}`);
console.log(`[http] MCP endpoint: ${mcpServerUrl.href}`);
console.log(`[http] Health check: ${PUBLIC_URL}/health`);
console.log(`[http] OAuth metadata: ${PUBLIC_URL}/.well-known/oauth-authorization-server`);
console.log(`[http] OAuth authorize: ${PUBLIC_URL}/authorize`);
console.log(`[http] OAuth token: ${PUBLIC_URL}/token`);
console.log(`[http] OAuth client ID: ${OAUTH_CLIENT_ID}`);
console.log(`[http] DCR disabled — only pre-registered client can connect`);
});

// ---------- Graceful shutdown ----------
const shutdown = async (signal: string) => {
console.log(`[http] Received ${signal}, shutting down...`);
for (const [sid, transport] of sessions) {
try { await transport.close(); } catch { /* ignore */ }
sessions.delete(sid);
}
httpServer.close(() => process.exit(0));
setTimeout(() => process.exit(1), 5000).unref();
};

process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}

main().catch((err) => {
console.error('[http] Fatal error:', err);
process.exit(1);
});
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ async function runServer() {
return;
}

// Check if first argument is "http" — Streamable HTTP transport
if (process.argv[2] === 'http') {
await import('./http-server.js');
return;
}

// Parse command line arguments for onboarding control
const DISABLE_ONBOARDING = process.argv.includes('--no-onboarding');
if (DISABLE_ONBOARDING) {
Expand Down
Loading