mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
refactor(server): providerize the OAuth SDK adapter behind the container
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
import { AddonsService } from './addons.service';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
|
||||
/**
|
||||
* Gate: 404 (empty body) when the MCP addon is disabled (M2 — prevents feature
|
||||
* fingerprinting). A factory over the injected AddonsService rather than a
|
||||
* middleware class: consumers build it inside their module's configure() and
|
||||
* hand the bare RequestHandler to consumer.apply() ahead of the SDK routers.
|
||||
*/
|
||||
export function createMcpAddonGate(addons: AddonsService): RequestHandler {
|
||||
return (_req, res, next) => {
|
||||
if (!addons.isAddonEnabled(ADDON_IDS.MCP)) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -1,36 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import type { OAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/provider';
|
||||
import type { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/sdk/server/auth/provider';
|
||||
import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth';
|
||||
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types';
|
||||
import type { AuthorizationParams } from '@modelcontextprotocol/sdk/server/auth/provider';
|
||||
import type { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients';
|
||||
import { InvalidClientMetadataError, ServerError } from '@modelcontextprotocol/sdk/server/auth/errors';
|
||||
import { db } from '../db/database';
|
||||
import {
|
||||
createOAuthClient,
|
||||
consumeAuthCode,
|
||||
issueTokens,
|
||||
refreshTokens,
|
||||
revokeToken as serviceRevokeToken,
|
||||
verifyPKCE,
|
||||
getUserByAccessToken,
|
||||
} from '../nest/oauth/oauth.bridge';
|
||||
import { ALL_SCOPES } from './scopes';
|
||||
import { getMcpSafeUrl } from '../app-config';
|
||||
import { writeAudit } from '../nest/audit/audit.bridge';
|
||||
import { OauthService } from './oauth.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { ALL_SCOPES } from '../../mcp/scopes';
|
||||
import { getMcpSafeUrl } from '../../app-config';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DB row type (mirrors oauthService.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface OAuthClientRow {
|
||||
client_id: string;
|
||||
name: string;
|
||||
redirect_uris: string; // JSON array
|
||||
allowed_scopes: string; // JSON array
|
||||
is_public: number; // 0 | 1
|
||||
created_via: string;
|
||||
}
|
||||
/**
|
||||
* TREK's adapters behind the MCP SDK's OAuth server interfaces, wrapping the
|
||||
* injected OauthService. The SDK's authorizationHandler / clientRegistrationHandler
|
||||
* Express routers are built over these instances in OauthModule.configure().
|
||||
*
|
||||
* Pending authorization codes stay module-scoped in oauth.pending-codes.ts: the
|
||||
* consent controller writes a code through the container singleton and
|
||||
* exchangeAuthorizationCode reads it back through the same injected singleton.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Redirect URI validation (mirrors oauth.ts DCR checks)
|
||||
@@ -60,7 +48,7 @@ function assertValidRedirectUris(uris: string[]): void {
|
||||
// Row → SDK client info shape
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function rowToInfo(row: OAuthClientRow): OAuthClientInformationFull {
|
||||
function rowToInfo(row: NonNullable<ReturnType<OauthService['getSdkClient']>>): OAuthClientInformationFull {
|
||||
return {
|
||||
client_id: row.client_id,
|
||||
client_name: row.name,
|
||||
@@ -76,13 +64,14 @@ function rowToInfo(row: OAuthClientRow): OAuthClientInformationFull {
|
||||
// Clients store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const trekClientsStore: OAuthRegisteredClientsStore = {
|
||||
@Injectable()
|
||||
export class TrekClientsStore implements OAuthRegisteredClientsStore {
|
||||
constructor(private readonly oauth: OauthService) {}
|
||||
|
||||
async getClient(clientId: string): Promise<OAuthClientInformationFull | undefined> {
|
||||
const row = db.prepare(
|
||||
'SELECT client_id, name, redirect_uris, allowed_scopes, is_public, created_via FROM oauth_clients WHERE client_id = ?'
|
||||
).get(clientId) as OAuthClientRow | undefined;
|
||||
const row = this.oauth.getSdkClient(clientId);
|
||||
return row ? rowToInfo(row) : undefined;
|
||||
},
|
||||
}
|
||||
|
||||
async registerClient(
|
||||
metadata: Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>,
|
||||
@@ -99,7 +88,7 @@ export const trekClientsStore: OAuthRegisteredClientsStore = {
|
||||
const scopes = rawScopes.filter(s => (ALL_SCOPES as string[]).includes(s));
|
||||
if (scopes.length === 0) throw new InvalidClientMetadataError('No valid scopes requested');
|
||||
|
||||
const result = createOAuthClient(null, name, uris, scopes, null, { isPublic, createdVia: 'dcr' });
|
||||
const result = this.oauth.createOAuthClient(null, name, uris, scopes, null, { isPublic, createdVia: 'dcr' });
|
||||
if (result.error) throw new InvalidClientMetadataError(result.error);
|
||||
|
||||
const c = result.client!;
|
||||
@@ -113,15 +102,22 @@ export const trekClientsStore: OAuthRegisteredClientsStore = {
|
||||
response_types: ['code'],
|
||||
...(c.client_secret ? { client_secret: c.client_secret as string, client_secret_expires_at: 0 } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OAuthServerProvider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const trekOAuthProvider: OAuthServerProvider = {
|
||||
get clientsStore() { return trekClientsStore; },
|
||||
@Injectable()
|
||||
export class TrekOAuthProvider implements OAuthServerProvider {
|
||||
constructor(
|
||||
private readonly clients: TrekClientsStore,
|
||||
private readonly oauth: OauthService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
get clientsStore(): OAuthRegisteredClientsStore { return this.clients; }
|
||||
|
||||
// Redirects browser to the SPA consent page with OAuth params forwarded.
|
||||
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
|
||||
@@ -149,15 +145,15 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
||||
|
||||
const base = getMcpSafeUrl().replace(/\/+$/, '');
|
||||
res.redirect(302, `${base}/oauth/consent?${qs.toString()}`);
|
||||
},
|
||||
}
|
||||
|
||||
// Not called because skipLocalPkceValidation = true.
|
||||
// PKCE verification is done inline in exchangeAuthorizationCode.
|
||||
skipLocalPkceValidation: true,
|
||||
readonly skipLocalPkceValidation = true;
|
||||
|
||||
async challengeForAuthorizationCode(_client: OAuthClientInformationFull, _code: string): Promise<string> {
|
||||
throw new ServerError('PKCE validation is handled by the provider directly');
|
||||
},
|
||||
}
|
||||
|
||||
async exchangeAuthorizationCode(
|
||||
client: OAuthClientInformationFull,
|
||||
@@ -166,7 +162,7 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
||||
redirectUri?: string,
|
||||
resource?: URL,
|
||||
): Promise<OAuthTokens> {
|
||||
const pending = consumeAuthCode(code);
|
||||
const pending = this.oauth.consumeAuthCode(code);
|
||||
if (!pending || pending.clientId !== client.client_id)
|
||||
throw new Error('Authorization grant is invalid.');
|
||||
|
||||
@@ -177,18 +173,18 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
||||
if (pending.resource && resourceStr && pending.resource !== resourceStr)
|
||||
throw new Error('Authorization grant is invalid.');
|
||||
|
||||
if (codeVerifier && !verifyPKCE(codeVerifier, pending.codeChallenge))
|
||||
if (codeVerifier && !this.oauth.verifyPKCE(codeVerifier, pending.codeChallenge))
|
||||
throw new Error('Authorization grant is invalid.');
|
||||
|
||||
const tokens = issueTokens(client.client_id, pending.userId, pending.scopes, null, pending.resource ?? null);
|
||||
writeAudit({
|
||||
const tokens = this.oauth.issueTokens(client.client_id, pending.userId, pending.scopes, null, pending.resource ?? null);
|
||||
this.audit.writeAudit({
|
||||
userId: pending.userId,
|
||||
action: 'oauth.token.issue',
|
||||
details: { client_id: client.client_id, scopes: pending.scopes, audience: pending.resource ?? null },
|
||||
ip: null,
|
||||
});
|
||||
return tokens;
|
||||
},
|
||||
}
|
||||
|
||||
async exchangeRefreshToken(
|
||||
client: OAuthClientInformationFull,
|
||||
@@ -196,13 +192,13 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
||||
_scopes?: string[],
|
||||
_resource?: URL,
|
||||
): Promise<OAuthTokens> {
|
||||
const result = refreshTokens(refreshToken, client.client_id, client.client_secret, null);
|
||||
const result = this.oauth.refreshTokens(refreshToken, client.client_id, client.client_secret, null);
|
||||
if (result.error) throw new Error(result.error === 'invalid_client' ? 'Invalid client credentials' : 'Refresh token is invalid or expired');
|
||||
return result.tokens!;
|
||||
},
|
||||
}
|
||||
|
||||
async verifyAccessToken(token: string): Promise<AuthInfo> {
|
||||
const info = getUserByAccessToken(token);
|
||||
const info = this.oauth.getUserByAccessToken(token);
|
||||
if (!info) throw new Error('Invalid or expired token');
|
||||
return {
|
||||
token,
|
||||
@@ -210,12 +206,12 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
||||
scopes: info.scopes,
|
||||
extra: { user: info.user },
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
async revokeToken(
|
||||
client: OAuthClientInformationFull,
|
||||
request: OAuthTokenRevocationRequest,
|
||||
): Promise<void> {
|
||||
serviceRevokeToken(request.token, client.client_id, undefined, null);
|
||||
},
|
||||
};
|
||||
this.oauth.revokeToken(request.token, client.client_id, undefined, null);
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,17 @@ import { DatabaseService } from '../database/database.service';
|
||||
import { OauthService } from './oauth.service';
|
||||
|
||||
/**
|
||||
* Non-Nest entry point for the OAuth domain — for code running OUTSIDE the
|
||||
* Nest container: the MCP transport (src/mcp/index.ts token verification) and
|
||||
* the SDK provider adapter (src/mcp/oauthProvider.ts), which
|
||||
* Non-Nest entry point for the OAuth domain — down to its last consumer: the
|
||||
* MCP transport's token verification (src/mcp/index.ts), which
|
||||
* platform.routes.ts mounts on the Express app BEFORE app.init(), so
|
||||
* `app.get(OauthService)` is not available to it. Exports the legacy
|
||||
* services/oauthService names 1:1 so repointing a consumer is an
|
||||
* import-path-only diff. Inside the container, inject OauthService instead.
|
||||
* Delete this file when the SDK mount moves behind the container.
|
||||
* `app.get(OauthService)` is not available to it. The SDK provider adapter
|
||||
* moved behind the container (oauth-sdk.provider.ts) and dropped the other six
|
||||
* exports. Inside the container, inject OauthService instead. Delete this file
|
||||
* when the /mcp mount moves behind the container.
|
||||
*
|
||||
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
|
||||
* the shared better-sqlite3 singleton. The one piece of shared state — the
|
||||
* pending authorization codes — is module-scoped in oauth.pending-codes.ts, so
|
||||
* this instance and the container singleton see the same map. That is load-
|
||||
* bearing: the consent controller writes the code through the container and
|
||||
* the SDK reads it back through here (which is why consumeAuthCode is bridged
|
||||
* and createAuthCode is not).
|
||||
* the shared better-sqlite3 singleton, and getUserByAccessToken touches no
|
||||
* module-scoped state beyond it.
|
||||
*/
|
||||
const dbs = new DatabaseService(db);
|
||||
const oauth = new OauthService(dbs, new AddonsService(dbs), new AuditService(dbs));
|
||||
@@ -28,27 +23,3 @@ const oauth = new OauthService(dbs, new AddonsService(dbs), new AuditService(dbs
|
||||
export function getUserByAccessToken(rawToken: string) {
|
||||
return oauth.getUserByAccessToken(rawToken);
|
||||
}
|
||||
|
||||
export function createOAuthClient(...args: Parameters<OauthService['createOAuthClient']>) {
|
||||
return oauth.createOAuthClient(...args);
|
||||
}
|
||||
|
||||
export function consumeAuthCode(code: string) {
|
||||
return oauth.consumeAuthCode(code);
|
||||
}
|
||||
|
||||
export function issueTokens(...args: Parameters<OauthService['issueTokens']>) {
|
||||
return oauth.issueTokens(...args);
|
||||
}
|
||||
|
||||
export function refreshTokens(...args: Parameters<OauthService['refreshTokens']>) {
|
||||
return oauth.refreshTokens(...args);
|
||||
}
|
||||
|
||||
export function revokeToken(...args: Parameters<OauthService['revokeToken']>) {
|
||||
return oauth.revokeToken(...args);
|
||||
}
|
||||
|
||||
export function verifyPKCE(codeVerifier: string, codeChallenge: string): boolean {
|
||||
return oauth.verifyPKCE(codeVerifier, codeChallenge);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,66 @@
|
||||
import { RateLimitModule } from '../common/rate-limit.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import type { MiddlewareConsumer, NestModule } from '@nestjs/common';
|
||||
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
|
||||
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
|
||||
import { OauthPublicController } from './oauth-public.controller';
|
||||
import { OauthApiController } from './oauth-api.controller';
|
||||
import { OauthService } from './oauth.service';
|
||||
import { TrekClientsStore, TrekOAuthProvider } from './oauth-sdk.provider';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { AddonsModule } from '../addons/addons.module';
|
||||
import { AddonsService } from '../addons/addons.service';
|
||||
import { createMcpAddonGate } from '../addons/mcp-addon-gate';
|
||||
|
||||
/**
|
||||
* OAuth 2.1 server (MCP). Public token/userinfo/revoke endpoints + the SPA's
|
||||
* authenticated consent/client/session management. /oauth/authorize,
|
||||
* /oauth/register and /oauth/consent are still served by the MCP SDK's own
|
||||
* router, mounted on the Express platform before app.init() — that path
|
||||
* reaches the domain through oauth.bridge.ts, which is why the pending
|
||||
* authorization codes live in a module, not on the provider.
|
||||
* authenticated consent/client/session management, plus the SDK's own
|
||||
* /oauth/authorize and /oauth/register Express routers, mounted here through
|
||||
* MiddlewareConsumer over the injected TrekOAuthProvider/TrekClientsStore.
|
||||
* forRoutes('<path>') is an Express prefix mount — the same app.use('/path')
|
||||
* shape (and the same req.url stripping) the pre-init platform mount used, and
|
||||
* exactly what the SDK routers are built for. The handlers are constructed once
|
||||
* per app instance in configure(), preserving the per-app lifetime of the DCR
|
||||
* handler's built-in rate limiter.
|
||||
*
|
||||
* Exports OauthService for AdminController, which serves the admin OAuth-session
|
||||
* panel under /api/admin. That is the one in-container consumer; everything
|
||||
* outside the container still goes through oauth.bridge.ts.
|
||||
* The handlers bring their own body parsers; the global 100kb parsers run
|
||||
* first and theirs no-op on the already-consumed stream. The one observable
|
||||
* difference vs the pre-init mount: bracketed keys in a POST /oauth/authorize
|
||||
* body now parse with qs (extended:true) rather than querystring — the SDK
|
||||
* zod-rejects such bodies either way.
|
||||
*
|
||||
* Pending authorization codes live module-scoped in oauth.pending-codes.ts:
|
||||
* the consent controller (container singleton) writes them, the SDK exchange
|
||||
* path reads them back through the same injected singleton.
|
||||
*
|
||||
* Exports OauthService for AdminController (admin OAuth-session panel) and the
|
||||
* MCP transport's token verification.
|
||||
*/
|
||||
@Module({
|
||||
imports: [RateLimitModule, AuditModule, AddonsModule],
|
||||
controllers: [OauthPublicController, OauthApiController],
|
||||
providers: [OauthService],
|
||||
providers: [OauthService, TrekClientsStore, TrekOAuthProvider],
|
||||
exports: [OauthService],
|
||||
})
|
||||
export class OauthModule {}
|
||||
export class OauthModule implements NestModule {
|
||||
constructor(
|
||||
private readonly addons: AddonsService,
|
||||
private readonly provider: TrekOAuthProvider,
|
||||
private readonly clients: TrekClientsStore,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
const mcpAddonGate = createMcpAddonGate(this.addons);
|
||||
|
||||
// SDK authorize handler: validates OAuth params, calls provider.authorize()
|
||||
// which redirects to the SPA consent page at /oauth/consent
|
||||
consumer
|
||||
.apply(mcpAddonGate, authorizationHandler({ provider: this.provider }))
|
||||
.forRoutes('oauth/authorize');
|
||||
|
||||
// SDK DCR handler: accepts registrations without scope (fixes issue #959 bug 2)
|
||||
consumer
|
||||
.apply(mcpAddonGate, clientRegistrationHandler({ clientsStore: this.clients }))
|
||||
.forRoutes('oauth/register');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* The short-lived authorization-code store.
|
||||
*
|
||||
* Module-scoped + import-time interval on purpose (legacy parity, shared
|
||||
* between bridge and container instances — atlas-geo interval precedent).
|
||||
* This is not a stylistic choice: the SDK-mounted /oauth/authorize path reads
|
||||
* a code through oauth.bridge (mcp/oauthProvider.ts) that the Nest consent
|
||||
* controller wrote through the container singleton. Two instances would mean
|
||||
* two maps, and the authorization-code flow would fail *silently* — the SDK
|
||||
* only ever answers "Authorization grant is invalid."
|
||||
* Module-scoped + import-time interval on purpose (legacy parity — atlas-geo
|
||||
* interval precedent). This is not a stylistic choice: the consent controller
|
||||
* writes a code through the container OauthService and the SDK exchange path
|
||||
* (oauth-sdk.provider.ts) reads it back — through the same injected singleton
|
||||
* in production, but the integration harness and any second OauthService
|
||||
* instance must keep seeing the same map. Two maps would make the
|
||||
* authorization-code flow fail *silently* — the SDK only ever answers
|
||||
* "Authorization grant is invalid."
|
||||
*
|
||||
* No DB persistence: codes live two minutes and a restart invalidating them
|
||||
* is the correct behaviour.
|
||||
|
||||
@@ -349,6 +349,22 @@ export class OauthService {
|
||||
// Token verification (used by MCP handler on every request)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** SDK clients-store read: the exact row shape the MCP SDK adapter
|
||||
* (oauth-sdk.provider.ts) maps to OAuthClientInformationFull. */
|
||||
getSdkClient(clientId: string): {
|
||||
client_id: string;
|
||||
name: string;
|
||||
redirect_uris: string;
|
||||
allowed_scopes: string;
|
||||
is_public: number;
|
||||
created_via: string;
|
||||
} | undefined {
|
||||
return this.db.get(
|
||||
'SELECT client_id, name, redirect_uris, allowed_scopes, is_public, created_via FROM oauth_clients WHERE client_id = ?',
|
||||
clientId,
|
||||
);
|
||||
}
|
||||
|
||||
getUserByAccessToken(rawToken: string): OAuthTokenInfo | null {
|
||||
const hash = hashToken(rawToken);
|
||||
const row = this.db.get<OAuthTokenRow & { username: string; email: string; role: string }>(`
|
||||
|
||||
@@ -6,11 +6,6 @@ import { readEnv } from '../../app-config';
|
||||
import { verifyJwtAndLoadUser } from '../auth/jwt-verify';
|
||||
import { db } from '../../db/database';
|
||||
import { mcpHandler } from '../../mcp';
|
||||
import { trekOAuthProvider, trekClientsStore } from '../../mcp/oauthProvider';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
|
||||
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
|
||||
|
||||
// Platform / transport routes extracted verbatim from createApp() (app.ts) so they can be
|
||||
// mounted on either the legacy Express app or the NestJS Express instance (strangler A6/A8).
|
||||
@@ -100,27 +95,14 @@ export function applyPlatformUploads(app: express.Application): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* The OAuth SDK + /mcp transport mounts still living on the pre-init Express
|
||||
* layer. The rest of the former transport surface is behind the container now:
|
||||
* /api/health (FeaturesController), OAuth discovery (DiscoveryController + the
|
||||
* McpMetadataMiddleware bootstrap applies pre-init), and the /oauth/consent
|
||||
* COOP override (ConsentCoopMiddleware via PlatformModule.configure).
|
||||
* The /mcp transport mounts — the last pre-init platform routes. Everything
|
||||
* else that used to live here is behind the container now: /api/health
|
||||
* (FeaturesController), OAuth discovery (DiscoveryController + the metadata
|
||||
* middleware bootstrap applies pre-init), /oauth/authorize + /oauth/register
|
||||
* (OauthModule.configure over the injected SDK adapters), and the
|
||||
* /oauth/consent COOP override (ConsentCoopMiddleware).
|
||||
*/
|
||||
export function applyPlatformTransport(app: express.Application): void {
|
||||
// OAuth 2.1 — public endpoints
|
||||
// Gate: 404 when MCP addon is disabled (M2 — prevents feature fingerprinting)
|
||||
const mcpAddonGate = (_req: Request, res: Response, next: NextFunction) => {
|
||||
if (!isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();
|
||||
next();
|
||||
};
|
||||
|
||||
// SDK authorize handler: validates OAuth params, calls provider.authorize() which redirects
|
||||
// to the SPA consent page at /oauth/consent
|
||||
app.use('/oauth/authorize', mcpAddonGate, authorizationHandler({ provider: trekOAuthProvider }));
|
||||
|
||||
// SDK DCR handler: accepts registrations without scope (fixes issue #959 bug 2)
|
||||
app.use('/oauth/register', mcpAddonGate, clientRegistrationHandler({ clientsStore: trekClientsStore }));
|
||||
|
||||
// MCP endpoint
|
||||
app.post('/mcp', mcpHandler);
|
||||
app.get('/mcp', mcpHandler);
|
||||
|
||||
Reference in New Issue
Block a user