dorfteich/apps/api/src/app.module.ts
Claude Fable 5 04e21a0aac
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
Built-in MCP endpoint (Streamable HTTP) on top of the public API (#105)
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
2026-07-12 11:36:02 +02:00

98 lines
3.7 KiB
TypeScript

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { LoggerModule } from 'nestjs-pino';
import { AdminModule } from './admin/admin.module';
import { AuditModule } from './audit/audit.module';
import { AuthModule } from './auth/auth.module';
import { BackupModule } from './backup/backup.module';
import { ApiExceptionFilter } from './common/api-exception.filter';
import { CommentsModule } from './comments/comments.module';
import { CompactionModule } from './compaction/compaction.module';
import { AppConfig } from './config/app-config.service';
import { ConfigModule } from './config/config.module';
import { FilesModule } from './files/files.module';
import { GrantsModule } from './grants/grants.module';
import { HealthModule } from './health/health.module';
import { ImportExportModule } from './import-export/import-export.module';
import { LabelsModule } from './labels/labels.module';
import { LegalModule } from './legal/legal.module';
import { LinksModule } from './links/links.module';
import { MailModule } from './mail/mail.module';
import { McpModule } from './mcp/mcp.module';
import { MembersModule } from './members/members.module';
import { PagesModule } from './pages/pages.module';
import { PermissionsModule } from './permissions/permissions.module';
import { PluginsModule } from './plugins/plugins.module';
import { PondsModule } from './ponds/ponds.module';
import { PrismaModule } from './prisma/prisma.module';
import { PublicApiModule } from './public-api/public-api.module';
import { PublicModule } from './public/public.module';
import { RateLimitModule } from './rate-limit/rate-limit.module';
import { SearchModule } from './search/search.module';
import { SettingsModule } from './settings/settings.module';
import { SetupModule } from './setup/setup.module';
import { TrashModule } from './trash/trash.module';
import { UsersModule } from './users/users.module';
import { NotificationsModule } from './notifications/notifications.module';
import { WatchesModule } from './watches/watches.module';
import { VersionsModule } from './versions/versions.module';
@Module({
imports: [
ConfigModule,
PrismaModule,
AuditModule,
RateLimitModule,
MailModule,
SettingsModule,
// Before SetupModule and AuthModule: global guards run in registration
// order, and the maintenance gate (in-app restore, issue #103) must
// answer before anything touches the mid-restore database.
BackupModule,
// Before AuthModule: the setup gate must win over AuthGuard's 401 while
// setup is pending.
SetupModule,
UsersModule,
PermissionsModule,
PondsModule,
PagesModule,
CommentsModule,
WatchesModule,
NotificationsModule,
FilesModule,
TrashModule,
CompactionModule,
VersionsModule,
LabelsModule,
LegalModule,
LinksModule,
SearchModule,
GrantsModule,
MembersModule,
PublicModule,
PublicApiModule,
McpModule,
ImportExportModule,
PluginsModule,
AuthModule,
AdminModule,
LoggerModule.forRootAsync({
inject: [AppConfig],
useFactory: (config: AppConfig) => ({
pinoHttp: {
level: config.env.LOG_LEVEL,
// Human-readable logs in local development, JSON everywhere else.
transport: config.env.NODE_ENV === 'development' ? { target: 'pino-pretty' } : undefined,
autoLogging: config.env.NODE_ENV !== 'test',
// Request bodies are never logged (operations.md logging rules).
redact: { paths: ['req.headers.authorization', 'req.headers.cookie'], remove: true },
},
}),
}),
HealthModule,
],
providers: [{ provide: APP_FILTER, useClass: ApiExceptionFilter }],
})
export class AppModule {}