Skip to content
6 changes: 6 additions & 0 deletions apps/api/src/__tests__/route-policy-enforcement.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/api/src/handlers/discord/__tests__/index.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions apps/api/src/handlers/discord/fast-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
import {
acquireFastAgentTurnLock,
answerFastAgentQuestion,
resolveApiBaseUrl,
} from '@roomote/cloud-agents/server';
import { Env } from '@roomote/env';
import { resolveUserMcpServerConfigs } from '@roomote/sdk/server';
import { ALL_REPOSITORIES } from '@roomote/types';

import { replyToDiscordEvent } from './replies.js';
Expand Down Expand Up @@ -104,6 +105,7 @@ export async function processDiscordFastAgentMessage(input: {
: [];
const message = getDiscordMessageCreate(input.event);
let didSendVisibleResponse = false;
const apiBaseUrl = resolveApiBaseUrl() ?? undefined;
const response = await answerFastAgentQuestion({
question: input.question,
threadContext: history.map((entry) => ({
Expand All @@ -114,7 +116,7 @@ export async function processDiscordFastAgentMessage(input: {
...(entry.botId ? { bot_id: entry.botId } : {}),
})),
userId: input.senderUserId,
apiBaseUrl: Env.TRPC_URL ?? Env.R_APP_URL,
apiBaseUrl,
conversation,
signal: releaseFastAgentLock.signal,
senderDisplayName:
Expand All @@ -127,6 +129,12 @@ export async function processDiscordFastAgentMessage(input: {
actingUserId: input.senderUserId,
conversation,
}),
resolveMcpServerConfigs: () =>
resolveUserMcpServerConfigs({
userId: input.senderUserId,
apiBaseUrl,
includeRoomote: true,
}),
launchTask: async ({
prompt,
environmentId,
Expand Down
35 changes: 34 additions & 1 deletion apps/api/src/handlers/mcp/__tests__/integration-mcp.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions apps/api/src/handlers/mcp/in-process-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Hono } from 'hono';

import type { Variables } from '../../types';
import type { McpAuth } from './middleware';
import { toMcpToolResult } from './proxy-utils';

type InProcessApp = Hono<{
Variables: Variables & { mcpAuth: McpAuth };
}>;

export type InProcessApiResult = {
ok: boolean;
status: number;
payload: Record<string, unknown>;
};

export function toolError(payload: Record<string, unknown>) {
return { ...toMcpToolResult(payload), isError: true as const };
}

/**
* Invoke API routers in-process on behalf of an MCP tool handler, with the
* caller's auth impersonated into the request context. The routers rethrow
* unexpected errors expecting an app-level handler, and this synthetic app is
* not behind the API server's onError, so it carries its own JSON handler and
* tolerates non-JSON responses.
*/
export async function invokeInProcessApi(options: {
auth: McpAuth;
mount: (app: InProcessApp) => void;
path: string;
init?: RequestInit;
}): Promise<InProcessApiResult> {
const app: InProcessApp = new Hono();
app.onError((error, c) => {
console.error('[mcp] Unhandled in-process API error:', error);
return c.json({ error: 'Internal server error' }, 500);
});
app.use('*', async (c, next) => {
c.set('authContext', options.auth.authContext);
c.set('mcpAuth', options.auth);
await next();
});
options.mount(app);

const response = await app.request(
`http://roomote.internal${options.path}`,
options.init,
);
const rawText = await response.text();
let rawPayload: unknown;
try {
rawPayload = JSON.parse(rawText) as unknown;
} catch {
rawPayload = { error: rawText || `Request failed (${response.status})` };
}
const payload =
rawPayload && typeof rawPayload === 'object' && !Array.isArray(rawPayload)
? (rawPayload as Record<string, unknown>)
: { result: rawPayload };

return { ok: response.ok, status: response.status, payload };
}

// status is always the numeric HTTP code; a body carrying its own `status`
// marker must not clobber it.
export function toolResultFromApi(result: InProcessApiResult) {
return result.ok
? toMcpToolResult(result.payload)
: toolError({ ...result.payload, status: result.status });
}
Loading
Loading