Skip to main content

api-endpoint-contract

The contract every new or modified API endpoint must follow so it is correct for the public OpenAPI spec, the MCP server (npm @trycompai/mcp-server), the ValidationPipe, and the docs. Triggers on "new endpoint", "add API", "new DTO", "@Body", "@RequirePermission", "MCP tool", "edit controller in apps/api", "OpenAPI", or whenever editing controllers under apps/api/src/.

Quellinformationen

Repository
trycompai/comp
Letzte Quellaktivität
28. Mai 2026 um 21:58
Erkannte Sprache von SKILL.md
Englisch
Sterne
2.057
Forks
440

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
api-endpoint-contract
description
The contract every new or modified API endpoint must follow so it is correct for the public OpenAPI spec, the MCP server (npm @trycompai/mcp-server), the ValidationPipe, and the docs. Triggers on "new endpoint", "add API", "new DTO", "@Body", "@RequirePermission", "MCP tool", "edit controller in apps/api", "OpenAPI", or whenever editing controllers under apps/api/src/.
# API Endpoint Contract (MCP-friendly NestJS endpoints) Every customer-facing endpoint in `apps/api/src/` ends up in three places: 1. **The OpenAPI spec** (`packages/docs/openapi.json`) — regenerated on every dev boot, consumed by Speakeasy. 2. **The MCP server** (`apps/mcp-server/`, published as `@trycompai/mcp-server` on npm) — generated daily from the OpenAPI spec. 3. **The runtime ValidationPipe** — accepts/rejects request bodies based on class-validator metadata. If any one of these three is wrong, the endpoint either silently breaks for agents (Claude Desktop, Cursor, Codex, etc.) or fails validation at runtime. **Follow this contract on every body-accepting endpoint.** ## The 11 rules ### 1. DTOs MUST be classes — never interfaces, never inline types ```ts // ❌ erased at runtime → empty MCP schema → agents blind-guess the body interface CreateConnectionDto { providerSlug: string; credentials?: Record<string, string | string[]>; } // ❌ same problem async updateConnection(@Body() body: { metadata?: Record<string, unknown> }) { ... } // ✅ class — survives to runtime, @nestjs/swagger can introspect it class CreateConnectionDto { @ApiProperty({ description: '...', example: 'aws' }) @IsString() providerSlug!: string; } ``` ### 2. Every DTO property carries BOTH decorator stacks The global `ValidationPipe` runs with `whitelist: true, forbidNonWhitelisted: true`. A class with `@ApiProperty` but no `class-validator` decorator has **zero "known" properties** — the pipe rejects every field with *"property X should not exist"*. The reverse (class-validator without `@ApiProperty`) generates an **empty MCP schema** and agents blind-guess the body. ```ts class FooDto { // ✅ both stacks @ApiProperty({ description: 'Name', example: 'foo' }) @IsString() name!: string; @ApiPropertyOptional({ description: 'Optional tag', example: 'beta' }) @IsOptional() @IsString() tag?: string; @ApiPropertyOptional({ type: 'object', additionalProperties: true }) @IsOptional() @IsObject() metadata?: Record<string, unknown>; @ApiProperty({ type: 'array', items: { type: 'string' } }) @IsArray() @IsString({ each: true }) services!: string[]; } ``` ### 3. Add `@ApiBody({ type: DtoClass })` on the endpoint `@nestjs/swagger` does NOT reliably infer the body type from `@Body() body: DtoClass` alone. Always declare it explicitly so the OpenAPI `requestBody.content.application/json.schema.$ref` resolves correctly. ```ts @Post() @RequirePermission('integration', 'create') @ApiOperation({ summary: 'Create an integration connection' }) @ApiBody({ type: CreateConnectionDto }) async createConnection( @Body() body: CreateConnectionDto, @OrganizationId() organizationId: string, ) { ... } ``` ### 4. Operation descriptions ≤ 240 characters `apps/api/src/openapi/seo-text.ts:71` (`toOperationDescription`) trims every `@ApiOperation.description` to **240 chars at a word boundary** for SEO/docs consistency. Anything longer gets cut mid-sentence and the trailing words are stripped — **including the actionable step at the end of your description**. Count chars; keep the key instruction in the first 240. ### 5. Use a clean MCP tool name when the auto-derived one is ugly Tool names are auto-derived from controller method names by `applyMcpToolNames` in `apps/api/src/openapi/public-docs-metadata.ts`. If the auto-name is generic or ugly, override: ```ts @Post(':id/auto-answer') @ApiExtension('x-speakeasy-mcp', { name: 'generate-questionnaire-answers' }) async triggerAutoAnswer(@Param('id') id: string) { ... } ``` Tool name budget: **52 chars max**, kebab-case. ### 6. Agent-callable endpoints must NOT be behind `SessionOnlyGuard` If your endpoint uses `@UseGuards(HybridAuthGuard, SessionOnlyGuard, PermissionGuard)`, API-key callers get a 403 — meaning the MCP tool exists but fails for every customer call. Either: - Remove `SessionOnlyGuard` if the endpoint should be agent-callable, OR - Disable the MCP tool entirely in `apps/mcp-server/.speakeasy/mcp-uploads-overlay.yaml` with `x-speakeasy-mcp: { disabled: true }`. ### 7. Long-running operations: async + poll, never sync wait Anything taking > ~30s should not block on a single tool call. Return a run handle and let the agent poll: ```ts // Trigger: returns immediately @Post(':id/auto-answer') async triggerAutoAnswer(@Param('id') id: string): Promise<TriggerResponseDto> { const handle = await tasks.trigger('auto-answer-task', { id, ... }); return { runId: handle.id, status: 'generating', totalQuestions, answeredQuestions }; } // Agent polls find-by-id until counts converge ``` The `@ApiOperation.description` should tell the agent the poll target (e.g. *"Poll GET /v1/X/:id until answeredQuestions equals totalQuestions"*). ### 8. File uploads from agents: presigned URL + s3Key — never inline base64 Base64-through-LLM is catastrophically slow and overflows the context window. For any endpoint that needs file bytes: ```ts // Endpoint accepts both for the web UI (fileData) and agents (s3Key): class UploadAndParseDto { @ApiPropertyOptional({ description: 'Base64 — web UI only. AI clients use s3Key.' }) @IsOptional() @IsString() fileData?: string; @ApiPropertyOptional({ description: 'Key returned by /v1/uploads/presign.' }) @IsOptional() @IsString() s3Key?: string; } // Service resolves whichever was provided: const bytes = dto.fileData ?? (dto.s3Key ? await uploadsService.readUploadAsBase64(orgId, dto.s3Key) : null); ``` The MCP overlay then strips `fileData` from the MCP tool input so agents are forced into the presigned path. Pattern is in `apps/mcp-server/.speakeasy/mcp-uploads-overlay.yaml`. ### 9. Sensitive endpoints are deny-listed from public docs — don't fight it `apps/api/src/openapi/public-docs-quality.ts` strips paths matching `/\/credentials(?:\/|$)/` and similar from `packages/docs/openapi.json`. Endpoints for rotating credentials, raw secrets, etc. **intentionally do not appear in MCP**. Add new sensitive paths to that deny-list if they handle secrets. ### 10. MCP-incompatible response shapes: disable the MCP tool in the overlay SSE streams (`@ApiProduces('text/event-stream')`) and binary file responses (`@Res() res.send(buffer)`) cannot be consumed by a single JSON-RPC tool call. Disable them for MCP only (HTTP endpoint stays for the web UI): ```yaml # apps/mcp-server/.speakeasy/mcp-uploads-overlay.yaml - target: "$.paths['/v1/questionnaire/auto-answer'].post" update: x-speakeasy-mcp: disabled: true ``` ### 11. Every endpoint MUST have a meaningful summary + description — it powers MCP discovery `@ApiOperation({ summary, description })` is **not optional**. `openapi-docs.spec.ts` (via `collectPublicOpenApiIssues` in `apps/api/src/openapi/public-docs-quality.ts`) **fails CI** if any non-excluded operation has: - an empty `summary` → `missingSummaries` - a missing `description` or SEO metadata → `missingMetadata` - SEO metadata outside 80–160 chars, or a title > 60 chars → `invalidSeo` This matters more now that the hosted MCP (Gram) uses **dynamic toolsets**: with 300+ tools the agent never sees them all — it runs a semantic `search` over tool **names + descriptions** and only loads matches. A tool with a weak or missing description is effectively **undiscoverable**. The description is the tool's only chance of being found. ```ts @ApiOperation({ summary: 'List compliance policies', // concise tool title description: "Returns the organization's compliance policies (SOC 2, ISO 27001, …) " + 'with status and owner. Use to review or audit policy coverage.', // what it does + when to use it }) ``` Write the description for the agent deciding *whether to call this tool*: state what it does and when to use it. (Keep it ≤ 240 chars — see Rule 4.) ## Workflow checklist when adding a body endpoint 1. Define a `class` DTO. Two decorator stacks on every field. Add `@ApiBody({ type: DtoClass })` on the endpoint. 2. Give the endpoint a meaningful `@ApiOperation({ summary, description })` — both required, CI-enforced by `openapi-docs.spec.ts`, and they power MCP dynamic-toolset discovery (Rule 11). Keep the description ≤ 240 chars (Rule 4). 3. If the auto-derived MCP tool name is ugly, set `@ApiExtension('x-speakeasy-mcp', { name: '...' })`. 4. If the endpoint requires session auth, decide: remove `SessionOnlyGuard`, or disable it for MCP via the overlay. 5. For long-running work, return a run handle and document the poll target. 6. For file uploads, accept `s3Key` and read via `UploadsService.readUploadAsBase64`. 7. **`bun run --filter '@trycompai/api' dev`** — your dev server regenerates `packages/docs/openapi.json` on boot. 8. **`git add packages/docs/openapi.json`** — commit the regenerated spec alongside your API change. The daily Speakeasy CI reads from this file; if it's stale, your new tool never reaches customers. 9. Sanity-check the new operation in the spec: ``` node -e 'const o=require("./packages/docs/openapi.json"); console.log(o.paths["/v1/your-path"]?.post?.requestBody?.content?.["application/json"]?.schema)' ``` If the schema is `undefined` or has empty `properties`, **stop** — fix the DTO before merging. ## Why this matters Every bug below was a real customer-visible MCP failure caught during the May 2026 audit: | Bug | Root cause | Rule that prevents it | |---|---|---| | PDF upload crashed with `Cannot read properties of undefined (reading 'replace')` | Inline `@Body()` type → empty schema → agent guessed wrong body | Rule 1 + 3 | | Auto-answer "schema is just an empty object" | DTO had class-validator only, no `@ApiProperty` | Rule 2 | | `create-connection` "Provider undefined not found" | DTO was an `interface`, not a class | Rule 1 | | `create-connection` "property X should not exist" 400 | Class converted, but I forgot class-validator decorators | Rule 2 | | `create-upload-url` description cut at "...then." | Description was 330 chars; `seo-text.ts` truncates at 240 | Rule 4 | | Agent uploads stuck for 15+ min on base64 encoding | Tool accepted `fileData` as the only file input | Rule 8 | | Agent calls SSE auto-answer and hangs | Tool was generated from `@ApiProduces('text/event-stream')` | Rule 10 | | Agent tries to start OAuth and gets 403 | Endpoint was behind `SessionOnlyGuard` but generated as MCP tool | Rule 6 | | Agent can't find a tool that exists (dynamic toolsets) | Endpoint had a missing/weak description → invisible to semantic search | Rule 11 | Follow the 10 rules and you avoid every one of these.
Auf GitHub ansehen