All checks were successful
CD / Build and push images (push) Successful in 3m50s
CI / Lint, typecheck, test (push) Successful in 4m2s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m37s
CI / Import/export fidelity gate (push) Successful in 47s
AI clients talk to the instance directly at /api/mcp — under the /api/ path (deviation from the issue's literal /mcp) so every existing reverse proxy already routes it; no deployment changes anywhere. - Transport: official @modelcontextprotocol/sdk server, STATELESS — each POST builds a fresh server+transport pair, no session store, replicas stay trivial; GET/DELETE answer 405. Auth per PAT bearer (#104 tokens), per-token rate limit (429 + Retry-After). - Own switches, independent of REST: instance mcp.enabled (admin settings, default off; off = 404, feature invisible) + pond setting mcpEnabled (pond-settings toggle, default off) — pinned independent in both directions by tests. - Tools (thin wrappers over the #104 services, same permission gates, audit-logged writes): list_ponds, list_pages, read_page, search, create_page, update_page (replace semantics through the collab-owned restore path — open editors converge), add_comment, list_labels, set_page_labels (exact replace), export_pond (link to the REST ZIP). Tool errors carry the api error codes; results carry stable slugs/ids. MCP resources stay the documented stage-2 stretch goal. - Deliberately on the SDK's low-level Server API with a hand-written tool table (mcp-tools.ts): the typed registerTool generics drove tsc out of memory in a program this size; manual Zod validation keeps the wire behavior explicit. - PublicApiService exposure filtering parameterized ('api' | 'mcp', shared pondFeatureEnabled helper) — one implementation, two switches. - Docs: "Connect Claude Code / MCP clients" section in public-api.md (claude mcp add one-liner + mcp-remote bridge for stdio clients). Verification: 8-test e2e pack driving the real MCP SDK client over Streamable HTTP against a listening api (initialize + tools/list, switch independence in both directions, anonymous/garbage 401, opt-in 404 semantics, page roundtrip incl. restore-NOTIFY, labels/comments, read scope blocked from writes with scope_required); live check through the web proxy against the seeded stack (tools list, create, read, update, search — LIVE CHECK PASSED); full api suite 61/61 files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
53 lines
2.3 KiB
TypeScript
53 lines
2.3 KiB
TypeScript
import { mkdtempSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
|
import cookieParser from 'cookie-parser';
|
|
|
|
import { AppModule } from '../app.module';
|
|
|
|
/**
|
|
* Boots the full application for e2e tests, mirroring main.ts middleware.
|
|
* Requires TEST_DATABASE_URL; DATABASE_URL is pointed at it so the app
|
|
* under test uses the test database. `customize` can override providers
|
|
* (e.g. inject a fake converter for the conversion queue tests, issue #62).
|
|
*/
|
|
export async function createTestApp(
|
|
customize?: (builder: TestingModuleBuilder) => TestingModuleBuilder,
|
|
): Promise<INestApplication> {
|
|
process.env.NODE_ENV = 'test';
|
|
if (process.env.TEST_DATABASE_URL) {
|
|
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
}
|
|
process.env.DATABASE_URL ??= 'postgresql://nobody:nothing@127.0.0.1:59999/absent';
|
|
// Fresh scratch directory per test file so upload tests never touch the
|
|
// repository or collide with each other.
|
|
process.env.UPLOADS_DIR ??= mkdtempSync(join(tmpdir(), 'dorfteich-uploads-'));
|
|
process.env.PLUGINS_DIR ??= mkdtempSync(join(tmpdir(), 'dorfteich-plugins-'));
|
|
|
|
const base = Test.createTestingModule({ imports: [AppModule] });
|
|
const moduleRef = await (customize ? customize(base) : base).compile();
|
|
const app = moduleRef.createNestApplication<NestExpressApplication>();
|
|
app.use(cookieParser());
|
|
// Mirrors main.ts: base64 Yjs page state needs more than Express's 100kb default.
|
|
app.useBodyParser('json', { limit: '8mb' });
|
|
// Mirrors main.ts: the public API (issue #104) declares its full path.
|
|
app.setGlobalPrefix('api/v1', {
|
|
exclude: ['api/public/v1', 'api/public/v1/{*path}', 'api/mcp'],
|
|
});
|
|
await app.init();
|
|
return app;
|
|
}
|
|
|
|
/** Extracts the dt_session cookie pair ("name=value") from a response. */
|
|
export function sessionCookieOf(res: { headers: Record<string, unknown> }): string {
|
|
const header = res.headers['set-cookie'];
|
|
const cookies = Array.isArray(header) ? header : [header].filter(Boolean);
|
|
const session = (cookies as string[]).find((c) => c.startsWith('dt_session='));
|
|
if (!session) throw new Error('response carries no dt_session cookie');
|
|
return session.split(';')[0]!;
|
|
}
|