Compare commits
3 Commits
1887d90821
...
75fe0533c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75fe0533c1 | ||
|
|
5d1072889f | ||
|
|
dfc53cd15e |
@@ -315,10 +315,13 @@ async function main(): Promise<void> {
|
|||||||
const backupService = new BackupService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo);
|
const backupService = new BackupService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo);
|
||||||
const restoreService = new RestoreService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo);
|
const restoreService = new RestoreService(serverRepo, projectRepo, secretRepo, userRepo, groupRepo, rbacDefinitionRepo, promptRepo, templateRepo);
|
||||||
|
|
||||||
// Auth middleware for global hooks
|
// Shared auth dependencies. Both the global auth hook and the per-route
|
||||||
const authMiddleware = createAuthMiddleware({
|
// preHandler on /api/v1/mcp/proxy must know how to resolve both session
|
||||||
findSession: (token) => authService.findSession(token),
|
// bearers AND mcpctl_pat_ bearers, or mcplocal→mcpd proxy calls with a
|
||||||
findMcpToken: async (tokenHash) => {
|
// McpToken will 401 at the route layer even though the global hook accepts them.
|
||||||
|
const authDeps = {
|
||||||
|
findSession: (token: string) => authService.findSession(token),
|
||||||
|
findMcpToken: async (tokenHash: string) => {
|
||||||
const row = await mcpTokenRepo.findByHash(tokenHash);
|
const row = await mcpTokenRepo.findByHash(tokenHash);
|
||||||
if (row === null) return null;
|
if (row === null) return null;
|
||||||
return {
|
return {
|
||||||
@@ -332,7 +335,8 @@ async function main(): Promise<void> {
|
|||||||
revokedAt: row.revokedAt,
|
revokedAt: row.revokedAt,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
const authMiddleware = createAuthMiddleware(authDeps);
|
||||||
|
|
||||||
// Server
|
// Server
|
||||||
const app = await createServer(config, {
|
const app = await createServer(config, {
|
||||||
@@ -436,7 +440,7 @@ async function main(): Promise<void> {
|
|||||||
registerMcpProxyRoutes(app, {
|
registerMcpProxyRoutes(app, {
|
||||||
mcpProxyService,
|
mcpProxyService,
|
||||||
auditLogService,
|
auditLogService,
|
||||||
authDeps: { findSession: (token) => authService.findSession(token) },
|
authDeps,
|
||||||
});
|
});
|
||||||
registerRbacRoutes(app, rbacDefinitionService);
|
registerRbacRoutes(app, rbacDefinitionService);
|
||||||
registerUserRoutes(app, userService);
|
registerUserRoutes(app, userService);
|
||||||
|
|||||||
@@ -46,7 +46,13 @@ export async function refreshProjectUpstreams(
|
|||||||
servers = await mcpdClient.get<McpdServer[]>(path);
|
servers = await mcpdClient.get<McpdServer[]>(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return syncUpstreams(router, mcpdClient, servers);
|
// Downstream upstream-proxy calls go through `mcpdClient` too. In HTTP-mode
|
||||||
|
// mcplocal the pod has no credentials of its own, so the default token on
|
||||||
|
// `mcpdClient` is an empty string — every /api/v1/mcp/proxy call would 401.
|
||||||
|
// Bind a per-request client with the caller's bearer so each McpdUpstream
|
||||||
|
// forwards the same identity that passed project discovery.
|
||||||
|
const upstreamClient = authToken ? mcpdClient.withToken(authToken) : mcpdClient;
|
||||||
|
return syncUpstreams(router, upstreamClient, servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ export class McpdClient {
|
|||||||
return new McpdClient(this.baseUrl, this.token, { ...this.extraHeaders }, timeoutMs);
|
return new McpdClient(this.baseUrl, this.token, { ...this.extraHeaders }, timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new client with a different Bearer token. The HTTP-mode mcplocal
|
||||||
|
* pod has no credentials of its own — each incoming client request carries
|
||||||
|
* its McpToken, and this method is how we thread that token through to the
|
||||||
|
* McpdUpstream instances created during project discovery.
|
||||||
|
*/
|
||||||
|
withToken(token: string): McpdClient {
|
||||||
|
return new McpdClient(this.baseUrl, token, { ...this.extraHeaders }, this.timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
async get<T>(path: string): Promise<T> {
|
async get<T>(path: string): Promise<T> {
|
||||||
return this.request<T>('GET', path);
|
return this.request<T>('GET', path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,21 +62,31 @@ export function registerProjectMcpEndpoint(app: FastifyInstance, mcpdClient: Mcp
|
|||||||
return existing.router;
|
return existing.router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP-mode mcplocal has no pod-level credentials — the default
|
||||||
|
// `mcpdClient.token` is an empty string. Every downstream call from this
|
||||||
|
// request (upstream discovery, LLM config fetch, prompt index for
|
||||||
|
// begin_session) has to use the CALLER's McpToken as the bearer, or mcpd
|
||||||
|
// rejects with 401. Build one per-request client here and thread it
|
||||||
|
// everywhere instead of sprinkling `.withToken(authToken)` at each call site.
|
||||||
|
const requestClient = authToken ? mcpdClient.withToken(authToken) : mcpdClient;
|
||||||
|
|
||||||
// Create new router or refresh existing one
|
// Create new router or refresh existing one
|
||||||
const router = existing?.router ?? new McpRouter();
|
const router = existing?.router ?? new McpRouter();
|
||||||
await refreshProjectUpstreams(router, mcpdClient, projectName, authToken);
|
await refreshProjectUpstreams(router, mcpdClient, projectName, authToken);
|
||||||
|
|
||||||
// Resolve project LLM model: local override → mcpd recommendation → global default
|
// Resolve project LLM model: local override → mcpd recommendation → global default
|
||||||
const localOverride = loadProjectLlmOverride(projectName);
|
const localOverride = loadProjectLlmOverride(projectName);
|
||||||
const mcpdConfig = await fetchProjectLlmConfig(mcpdClient, projectName);
|
const mcpdConfig = await fetchProjectLlmConfig(requestClient, projectName);
|
||||||
const resolvedModel = localOverride?.model ?? mcpdConfig.llmModel ?? undefined;
|
const resolvedModel = localOverride?.model ?? mcpdConfig.llmModel ?? undefined;
|
||||||
|
|
||||||
// If project llmProvider is "none", disable LLM for this project
|
// If project llmProvider is "none", disable LLM for this project
|
||||||
const llmDisabled = mcpdConfig.llmProvider === 'none' || localOverride?.provider === 'none';
|
const llmDisabled = mcpdConfig.llmProvider === 'none' || localOverride?.provider === 'none';
|
||||||
const effectiveRegistry = llmDisabled ? null : (providerRegistry ?? null);
|
const effectiveRegistry = llmDisabled ? null : (providerRegistry ?? null);
|
||||||
|
|
||||||
// Configure prompt resources with SA-scoped client for RBAC
|
// Configure prompt resources with SA-scoped client for RBAC.
|
||||||
const saClient = mcpdClient.withHeaders({ 'X-Service-Account': `project:${projectName}` });
|
// Keep the X-Service-Account header for mcpd-side audit tagging, but carry
|
||||||
|
// the caller's bearer so auth passes (the principal resolves as McpToken:<sha>).
|
||||||
|
const saClient = requestClient.withHeaders({ 'X-Service-Account': `project:${projectName}` });
|
||||||
router.setPromptConfig(saClient, projectName);
|
router.setPromptConfig(saClient, projectName);
|
||||||
|
|
||||||
// System prompt fetcher for LLM consumers (uses router's cached fetcher)
|
// System prompt fetcher for LLM consumers (uses router's cached fetcher)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function mockMcpdClient(servers: Array<{ id: string; name: string; transport: st
|
|||||||
forward: vi.fn(async () => ({ status: 200, body: servers })),
|
forward: vi.fn(async () => ({ status: 200, body: servers })),
|
||||||
withTimeout: vi.fn(() => client),
|
withTimeout: vi.fn(() => client),
|
||||||
withHeaders: vi.fn(() => client),
|
withHeaders: vi.fn(() => client),
|
||||||
|
withToken: vi.fn(() => client),
|
||||||
};
|
};
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ function mockMcpdClient() {
|
|||||||
delete: vi.fn(),
|
delete: vi.fn(),
|
||||||
forward: vi.fn(async () => ({ status: 200, body: [] })),
|
forward: vi.fn(async () => ({ status: 200, body: [] })),
|
||||||
withHeaders: vi.fn(),
|
withHeaders: vi.fn(),
|
||||||
|
withToken: vi.fn(),
|
||||||
|
withTimeout: vi.fn(),
|
||||||
};
|
};
|
||||||
// withHeaders returns a new client-like object (returns self for simplicity)
|
// Chainable withX returns the same client for simplicity
|
||||||
(client.withHeaders as ReturnType<typeof vi.fn>).mockReturnValue(client);
|
(client.withHeaders as ReturnType<typeof vi.fn>).mockReturnValue(client);
|
||||||
|
(client.withToken as ReturnType<typeof vi.fn>).mockReturnValue(client);
|
||||||
|
(client.withTimeout as ReturnType<typeof vi.fn>).mockReturnValue(client);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user