Skip to content

feat(mcp): agent-first MCP surface, flow-spec DSL, capability discovery - #1184

Merged
realcodesiman merged 38 commits into
mainfrom
feat/mcp-agent-first
Sep 15, 2026
Merged

realcodesiman merged 38 commits into
mainfrom
feat/mcp-agent-first

Conversation

@realcodesiman

@realcodesiman realcodesiman commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an agent-first MCP surface on top of the public API: capability discovery (capabilities.*), a flow-spec DSL for authoring flows without hand-building node/edge graphs, and token introspection.
  • Curates which public API endpoints are exposed as default MCP tools (spec: mcpSpec({ visibility: "default" })), favoring read/discovery operations and moving heavier write operations behind explicit discovery via search_tools.
  • Adds description metadata to public oRPC routes so MCP tool listings carry useful context for an LLM caller, and refreshes apps/mcp-server/README.md to match the resulting default tool set.
  • Review follow-up (this update): completes description/tag/field-level .describe() coverage across all 395 public API operations (previously 114/395 had a description, 69 had no tags, 19 total .describe() calls existed), hardens the MCP server (fetch timeouts, request body size limit, safe query-param serialization for arrays/objects, negative token-introspection caching, composite allOf/anyOf schema flattening so POST bodies without a path param no longer yield an empty tool inputSchema), and cleans up the flow-spec DSL/capabilities layer (dead exports, duplicated "unknown name" resolution, a template-status gap that let PENDING/REJECTED WhatsApp templates compile into a flow, and closes .describe() gaps in the schema whose own header comment claimed full coverage).

Changes

  • packages/flow-config/src/authoring/*: flow-spec DSL schema, compiler (spec → node/edge graph), layout, and structured error reporting. This update: tightens the package's export surface (drops several over-exported internals), deduplicates three copies of "unknown name" resolution into one resolveByName helper, splits compileFlowSpec into focused stages, fixes a stale comment about layout overwriting measured, adds .describe() to every remaining schema field (button/case id, when, cases, object-level descriptions), and rejects flow-spec templates that are not APPROVED (templateNotApproved) instead of silently compiling them.
  • packages/business/src/capabilities/*: capability discovery service. This update adds packages/business/src/capabilities/schema.ts — every capability response field described via zod, replacing a hand-duplicated JSON-schema block in the builder route.
  • apps/builder/src/features/capabilities/api/public.ts, apps/builder/src/features/token/api/public.ts: new public endpoints for capability discovery and token introspection.
  • apps/builder/src/features/flows/api/public.ts, apps/builder/src/features/flows/lib/compile-spec-to-graph.ts: flows.validate and flows.updateDraft support the flow-spec DSL; flows.validate is now a default-visible MCP tool (44 default tools total).
  • apps/builder/src/lib/orpc/mcp-annotations.ts: mcpSpec helper for annotating which routes are exposed as default MCP tools.
  • apps/builder/src/lib/public-api/list.ts, new apps/builder/src/lib/public-api/params.ts: shared pagination/list-response description helpers (withListPagingNote) and an id-parameter description helper (publicIdParam/describeId) used across ~80 call sites instead of undescribed inline zodBigintAsString() literals.
  • apps/mcp-server/src/*: fetch timeouts (fetchWithTimeout + CHATBOTX_HTTP_TIMEOUT_MS), bounded request body parsing, safe rethrow instead of an unhandled rejection in the SSE server, trimmed env fallback token, search_tools limit no longer resolves to NaN, call_tool arguments validated as an object, shared errorResult/jsonResult helpers replacing 7 hand-rolled response blocks, Map-backed tool lookup, single-pass visible-tools filtering, extracted parseOperation, SDK isInitializeRequest instead of a local copy, and query-parameter serialization that no longer mangles arrays/objects for GET requests with array/object query params.
  • Public API routers — every feature directory under apps/builder/src/features/*/api/public.ts (and enterprise inbox-teams): every one of the 395 operations now has a description (1–3 sentences, cross-referencing sibling operations), every operation has at least one tag, and every top-level request field (path/query parameters and body properties) has a .describe() call at its schema source.

Follow-ups (deferred, not in this PR)

  • Extract the OpenAPI loader + request executor shared by apps/cli (openapi-loader.ts, dynamic-executor.ts) and apps/mcp-server into a workspace package.
  • apps/cli's CHATBOTX_ALLOW_SELF_SIGNED_CERT is read (apps/cli/src/config.ts:83-86) but never applied — apps/mcp-server's equivalent is implemented in this PR; the CLI copy still needs wiring up.
  • whatsappMessageTemplateService.list accepts a where.status filter parameter but never applies it.
  • tagService.listActive / whatsappMessageTemplateService.list have no limit parameter; capabilities.get currently slices the full result set in memory.

Test plan

  • pnpm --filter @chatbotx.io/flow-config test (540 tests)
  • pnpm --filter @chatbotx.io/business test (2617 tests)
  • pnpm --filter chatbotx-mcp-server test (79 tests)
  • pnpm --filter builder test (full suite, 3838 tests)
  • pnpm --filter @chatbotx.io/flow-config check-types && pnpm --filter @chatbotx.io/business check-types && pnpm --filter chatbotx-mcp-server check-types && pnpm --filter chatbotx-mcp-server build && pnpm --filter builder check-types
  • pnpm lint (root: agent-instructions/skills check, turbo lint, ultracite check over 6623 files)
  • Manual: ran apps/builder dev server, fetched /api/public-spec.json and confirmed every parameter and body property carries a description; loaded the spec through apps/mcp-server's OpenAPI loader against the local dev URL and confirmed every tool description includes the scope requirement and every input field is described; spot-checked /v1/broadcasts/{id}/schedule (id under parameters, schedulesType/schedulesAt under body, all described).

- collapse mcp tool surface from ~385 to 44 default tools via x-mcp
  openapi extension; search_tools/call_tool meta-tools for the long tail
- add flow-spec authoring dsl + compiler (packages/flow-config/authoring)
  wired into flows.publish/updateDraft/validate
- add capabilities.get, schemas.flowSpec, token.get discovery endpoints
- add scope- and permission-based tools/list filtering in mcp-server
  with cached token introspection
- fix mcp-server per-request token resolution, cors origin, docker env vars
Critical/Important fixes:
- flows.publish/validate {spec} path: compileAndValidateSpec now runs
  publishFlowSchema (channel rules) against the compiled graph and remaps
  zod issues back to spec-relative paths, so a channel-rule violation
  returns a structured 422 instead of a raw 500. orpc.ts also gains a
  ZodError safety net.
- action.setCustomField now resolves the field name via the same
  candidates-on-miss path as branch conditions, instead of passing the
  raw name through as inputFieldId.
- mcp-server token introspection response is zod-validated instead of
  blindly cast, and the introspection cache evicts expired entries.
- read-only token visibility now derives from the x-mcp.readOnlyHint
  annotation (contacts.search opts in) instead of a hard-coded tool-name
  allowlist.
- META_TOOL_NAMES / NO_BODY_METHODS use Set.has() instead of `in` on a
  plain object, closing a prototype-chain lookup risk.

Cleanup:
- remove dead FlowAuthoringContext fields, schema aliases, and the
  hand-written flow-spec step-type table (now derived from the schema);
  un-export McpVisibility/DynamicToolAnnotations/TokenScopeIntrospection.
- search_tools no longer requires an API key; its scoring precomputes
  per-tool tokens in a WeakMap; flow-spec JSON Schema is memoized.
- strip stale plan-section references and fix inaccurate route/operation
  counts in comments and docs.

Tests: new create-mcp-server.test.ts and capabilities-service.test.ts,
plus expanded coverage in compile.test.ts, flows-public-api.test.ts,
orpc-error-mapping.test.ts, token-introspection.test.ts,
openapi-loader.test.ts, and public-spec-mcp.test.ts.
Removes mcpSpec from write-heavy/less agent-friendly endpoints (broadcast
create/schedule, keyword create, contact upsert/setCustomFields, tag
add-by-id, custom field create, trigger create, flow validate, message
create) and adds it to more read-oriented ones (AI agents/files/functions,
analytics, sequences, error logs, contact tags/custom-fields/messages),
plus adds `description` metadata so MCP tool discovery has better context.
Refreshes the mcp-server README to match the resulting default tool list.
@github-actions github-actions Bot added the feature New feature or request label Sep 15, 2026
…hema

Splits compileFlowSpec into createCompileState/validateStructure/compileChain/finalizeGraph, extracts resolveByName to replace three duplicated lookup blocks, builds nodes immutably before registerNode instead of post-hoc mutation, makes applyRouteUpdatesInNodes generic, gates sendTemplate steps on APPROVED template status, describes every flow-spec schema field, drops unused button/case id fields, tightens the authoring package's public export surface, and adds layout/errors/compile edge-case test coverage.
Adds packages/business/src/capabilities/schema.ts with a fully-described zod schema per entity (inbox/template/field/namedEntity/flowSpec/response), replacing the hand-maintained response types in service.ts and the duplicated response schema in the builder's public capabilities route. DEFAULT_INCLUDES is now derived from CAPABILITIES_INCLUDES minus an explicit opt-in list instead of hand-copied. Fixes the capabilities.get and schemas.flowSpec route descriptions (bot fields are reference-only; flows.publish/updateDraft/validate accept spec, not flows.create).
…glue

apps/mcp-server: bounds request bodies (413), adds fetch timeouts (AbortSignal), negative-caches failed token introspection, fixes NaN-limit and array-arguments bugs in meta-tools, serializes query params recursively (bracket notation) instead of String(), merges allOf/anyOf composite body schemas into tool input schemas, appends a token-scope paragraph to tool descriptions, replaces the hand-rolled result/lookup/dispatch helpers with shared ones, adopts the SDK's isInitializeRequest, wraps the request listener in try/catch, trims CHATBOTX_API_KEY at parse time, implements the previously-dead CHATBOTX_ALLOW_SELF_SIGNED_CERT env var, and fixes stale flows_create/tool-count doc drift in README/SKILL. apps/builder: compile-spec-to-graph.ts's two public compilers now share a private compileWithContext helper instead of duplicating context-fetch + compile; public-spec-mcp.test.ts imports the real WORKSPACE_TOKEN_SECURITY_SCHEMES instead of redeclaring it and gains a parity check between the mcp-server docs' tool tables and the live default tool set.
…ows.validate

WS1 batch 0 infra: apps/builder/src/lib/public-api/list.ts describes publicListResponse's data/pageCount and gains PUBLIC_LIST_PAGING_NOTE/withListPagingNote; new lib/public-api/params.ts adds publicIdParam/describeId factories for the id-param pattern repeated across public routers. flows.validate becomes a default MCP tool (44 total) alongside description improvements on its flows.list/get/create/duplicate/publish siblings. public-spec-operations.test.ts and public-spec-mcp.test.ts gain description/tag/summary-style/field-description assertions gated by a DESCRIPTION_BACKLOG ratchet that subsequent batches shrink. mcp-server README/SKILL tool count corrected to 44 with flows_validate listed. Also carries partial in-progress summary/description work on several other routers (ads, ai-agents, analytics, appointments, broadcasts, capabilities, contacts, conversations, coupons, error-logs, media-library, product-categories, sequences, tags, token, triggers) that follow-up commits complete.
…hared pagination

WS1 batch 1 (partial): apps/builder/src/features/contacts/api/public/{crud,custom-fields,messages}.ts and their schema files gain description, tags, house-style summaries, and .describe() on every top-level input field. apps/builder/src/lib/pagination/index.ts describes page/perPage/sort/cursor on the shared basePaginationRequest/cursorPaginationRequest base schemas used across many public routers.
WS1 batch 1 (partial): apps/builder/src/features/contacts/api/public/tags.ts and its schema gain description, tags, house-style summaries, and .describe() on every top-level input field.
…ch 1)

Finishes WS1 batch 1: describes contacts.create/update/import/sendMessage's underlying shared schemas (schema/action.ts, schema/contact-import.ts, messages/schema/mutation.ts), and adds description+tags+house-style summaries+field descriptions to the three sibling routers mounted under the contacts.* operationId namespace (contact-notes, contact-inboxes, contact-sequences). Normalizes the two remaining .meta({ description }) sites in messages/schema/{mutation,public}.ts to .describe(). Removes "contacts." from public-spec-operations.test.ts's DESCRIPTION_BACKLOG ratchet — all 47 contacts.* operations now pass every description/tag/summary-style/field-description assertion.
WS1 batch 2 (partial): finishes apps/builder/src/features/flows/api/public.ts (delete/versions descriptions, publicIdParam for every {id} route, import field descriptions) and describes createFlowSchema/updateFlowSchema/updateDraftFlowVersionSchema/publishFlowSchema fields in schema/action.ts. All 11 flows.* operations now have description+tags+house-style summary+described fields.
WS1 batch 2 (partial): all 7 sequences.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field, including the nested sequence-step schema.
Removes "flows." and "sequences." from DESCRIPTION_BACKLOG now that both routers fully satisfy every description/tag/summary-style/field-description assertion. Lengthens sequences.deleteStep's description past the 50-char minimum.
WS1 batch 2 (partial): all 6 keywords.* (automated-response) operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "keywords." from DESCRIPTION_BACKLOG.
WS1 batch 2 (partial): all 5 aiAgents.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field including createAIAgentRequest/updateAIAgentRequest. Removes "aiAgents." from DESCRIPTION_BACKLOG.
…coverage

WS1 batch 2 (partial): all 4 aiFiles.* and 5 aiFunctions.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "aiFiles." and "aiFunctions." from DESCRIPTION_BACKLOG.
…n coverage

WS1 batch 2 finale: all 5 aiMcpServers.* and 6 triggers.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "aiMcpServers." and "triggers." from DESCRIPTION_BACKLOG, completing WS1 batch 2 (flows, sequences, keywords, aiAgents, aiFiles, aiFunctions, aiMcpServers, triggers) in full.
The contactFilter field description in schema/action.ts and schema/query.ts referenced a nonexistent `contactFilter.get` operation. The real lookup endpoint is `contacts.listFilterFields` (GET /v1/contacts/filter-fields, contactsFilterFieldsPublicRouter.listFilterFields).
WS1 batch 3 (partial): all 13 conversations.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "conversations." from DESCRIPTION_BACKLOG.
WS1 batch 3 (partial): all 6 messages.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field including editMessageRequest/changeMessageAttributesRequest. Removes "messages." from DESCRIPTION_BACKLOG.
WS1 batch 3 (partial): all 14 broadcasts.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field, including the large createBroadcastRequest schema. Rewrites the moveToDraft/resume bare status-constraint fragments into full call-order descriptions per user decision #3. Removes "broadcasts." from DESCRIPTION_BACKLOG.
WS1 batch 3 (partial): all 5 savedReplies.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "savedReplies." from DESCRIPTION_BACKLOG.
WS1 batch 3 (partial): both inboxes.* operations (list, listChannels) already had descriptions; describes the shared listInboxesRequest fields in packages/business/src/inbox/schema.ts. Removes "inboxes." from DESCRIPTION_BACKLOG.
WS1 batch 3 finale: all 7 inboxTeams.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "inboxTeams." from DESCRIPTION_BACKLOG, completing WS1 batch 3 (conversations, messages, broadcasts, savedReplies, inboxes, inboxTeams) in full.
WS1 batch 4 (partial): all 31 analytics.* operations gain description, tags, and house-style summaries. Describes the shared time-range/granularity/broadcast/sequence/flow/magic-link request schemas in packages/analytics/src/schemas once at the source, covering every analytics route's field-description requirement. Removes "analytics." from DESCRIPTION_BACKLOG.
…overage

WS1 batch 4 (partial): capabilities.get and schemas.flowSpec already had descriptions from the WS3 commit; describes the include query param's array schema. Removes "capabilities." and "schemas." from DESCRIPTION_BACKLOG.
…lWebhooks coverage

WS1 batch 4 finale: token.get already had a description; errorLogs/workspaceMembers/webhooks/externalWebhooks operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes the remaining batch 4 prefixes from DESCRIPTION_BACKLOG, completing WS1 batch 4 (analytics, capabilities, token, schemas, errorLogs, workspaceMembers, webhooks, externalWebhooks) in full.
WS1 batch 5 (partial): all 31 ads.* operations (16 from ads.ts's conversion-rules/analytics routers, 15 from ads-campaign.ts's messaging-ads router, both spread into the same adsPublicRouter) gain description, tags, house-style summaries, and .describe() on every top-level input field. Describes the shared ads-conversion-rule/ads-retarget request schemas in packages/business at their source. Removes "ads." from DESCRIPTION_BACKLOG.
WS1 batch 5 finale: all 7 facebookLeadAds.* operations gain description, tags, house-style summaries, and .describe() on every top-level input field. Removes "facebookLeadAds." from DESCRIPTION_BACKLOG, completing WS1 batch 5 (ads, ads-campaign, facebookLeadAds) in full — 38 operations.
realcodesiman and others added 9 commits September 15, 2026 12:15
WS1 batch 6 (partial): all 11 mediaLibrary.* operations gain description, tags, and .describe() on every top-level input field. Removes "mediaLibrary." from DESCRIPTION_BACKLOG.
WS1 batch 6 (partial): all 6 dynamicImages.* operations gain description, tags, and .describe() on every top-level input field. Removes "dynamicImages." from DESCRIPTION_BACKLOG.
WS1 batch 6 (partial): all 5 qrCodes.* operations gain description, tags, and .describe() on every top-level input field. Removes "qrCodes." from DESCRIPTION_BACKLOG.
WS1 batch 6 (partial): all 10 minigames.* operations gain description, tags, and .describe() on every top-level input field. Also describes the shared bulkUpdateIdsRequest.ids field at its source (used by minigames.deleteMany). Removes "minigames." from DESCRIPTION_BACKLOG.
WS1 batch 6 (final): all 11 coupons.* operations gain description, tags, and .describe() on every top-level input field. Removes "coupons." from DESCRIPTION_BACKLOG, completing batch 6.
WS1 batch 7: all appointmentCalendars/appointments/appointmentExternalCalendars/appointmentReminders/questionnaires/spreadsheets/emailTopics/botFields operations gain description, tags, and .describe() on every top-level input field. Removes their prefixes from DESCRIPTION_BACKLOG.
WS1 batch 8: all contactScans/customFields/folders/productCategories/products/reflinks/tags/userPersistentMenus operations gain description, tags, and .describe() on every top-level input field. Removes their prefixes from DESCRIPTION_BACKLOG.
WS1 batch 9 (final): all fbComments/igComments/igStories/channels/messengerChannels/smtpIntegrations/webchats/templateMessages/zaloChannels/integrations/messengerPersonas operations gain description, tags, and .describe() on every top-level input field. This empties DESCRIPTION_BACKLOG, so batch 10's ratchet-removal is folded in: deletes the DESCRIPTION_BACKLOG const and isDescriptionBacklogged helper, and un-conditions the description/tag/input-field coverage tests to run against every operation unconditionally. All 395 WS1 public API operations now have description, tags, and field-level .describe() coverage.
Fixes correctness bugs found in code review: missing successStatus:204
on the email-topics delete route, publishFlowRequest/updateDraftFlowRequest
silently discarding `spec` when a mixed body matches the graph union branch
first, capabilities list limit silently clamped to 50 instead of the
documented 200, and the flow compiler allowing goto self-loop edges plus
failing to resolve a step's own id for a same-step "back to menu" goto.
Adds guard tests for each and closes the docs gap for the new flow-spec
DSL and capability discovery surfaces.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@realcodesiman
realcodesiman merged commit 85bb4aa into main Sep 15, 2026
4 checks passed
@realcodesiman
realcodesiman deleted the feat/mcp-agent-first branch September 15, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant