feat(mcp): agent-first MCP surface, flow-spec DSL, capability discovery - #1184
Merged
Merged
Conversation
- 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.
…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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
capabilities.*), a flow-spec DSL for authoring flows without hand-building node/edge graphs, and token introspection.spec: mcpSpec({ visibility: "default" })), favoring read/discovery operations and moving heavier write operations behind explicit discovery viasearch_tools.descriptionmetadata to public oRPC routes so MCP tool listings carry useful context for an LLM caller, and refreshesapps/mcp-server/README.mdto match the resulting default tool set..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, compositeallOf/anyOfschema flattening so POST bodies without a path param no longer yield an empty toolinputSchema), 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 oneresolveByNamehelper, splitscompileFlowSpecinto focused stages, fixes a stale comment about layout overwritingmeasured, adds.describe()to every remaining schema field (button/caseid,when,cases, object-level descriptions), and rejects flow-spec templates that are notAPPROVED(templateNotApproved) instead of silently compiling them.packages/business/src/capabilities/*: capability discovery service. This update addspackages/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.validateandflows.updateDraftsupport the flow-spec DSL;flows.validateis now a default-visible MCP tool (44 default tools total).apps/builder/src/lib/orpc/mcp-annotations.ts:mcpSpechelper for annotating which routes are exposed as default MCP tools.apps/builder/src/lib/public-api/list.ts, newapps/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 inlinezodBigintAsString()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_toolslimit no longer resolves toNaN,call_toolarguments validated as an object, sharederrorResult/jsonResulthelpers replacing 7 hand-rolled response blocks,Map-backed tool lookup, single-pass visible-tools filtering, extractedparseOperation, SDKisInitializeRequestinstead of a local copy, and query-parameter serialization that no longer mangles arrays/objects for GET requests with array/object query params.apps/builder/src/features/*/api/public.ts(and enterpriseinbox-teams): every one of the 395 operations now has adescription(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)
apps/cli(openapi-loader.ts,dynamic-executor.ts) andapps/mcp-serverinto a workspace package.apps/cli'sCHATBOTX_ALLOW_SELF_SIGNED_CERTis 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.listaccepts awhere.statusfilter parameter but never applies it.tagService.listActive/whatsappMessageTemplateService.listhave nolimitparameter;capabilities.getcurrently 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-typespnpm lint(root: agent-instructions/skills check, turbo lint, ultracite check over 6623 files)apps/builderdev server, fetched/api/public-spec.jsonand confirmed every parameter and body property carries adescription; loaded the spec throughapps/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(idunder parameters,schedulesType/schedulesAtunder body, all described).