Skip to content

Agent-first: REST + MCP coverage of the dashboard surface, both APIs on oRPC#139

Open
tifa2UP wants to merge 12 commits into
mainfrom
feat/agent-first-api-mcp
Open

Agent-first: REST + MCP coverage of the dashboard surface, both APIs on oRPC#139
tifa2UP wants to merge 12 commits into
mainfrom
feat/agent-first-api-mcp

Conversation

@tifa2UP

@tifa2UP tifa2UP commented Jul 2, 2026

Copy link
Copy Markdown
Member

Everything that can be done in the dashboard can now be done via the public REST API and a hosted MCP server, so agents can set up and operate Agentset autonomously — with no breaking changes to existing endpoints. All three surfaces are now served by one oRPC router: the dashboard RPC, the public REST API, and the MCP server run the same procedures, with a single conditional-auth middleware deciding between API-key and session credentials.

Architecture: one router, three surfaces

Surface Before Now
Dashboard (/api/rpc) tRPC v11 + superjson + httpBatchStreamLink oRPC RPCHandler over the app router; session resolved once per HTTP request; per-batch-item headers via RequestHeadersPlugin
Public REST (api.agentset.ai/v1) 23 hand-written route handlers + withApiHandler wrappers oRPC OpenAPIHandler over the same app router, filtered to route-bearing procedures
/openapi.json 58 hand-maintained zod-openapi files, drift-prone vs routes generated from the same procedures (OpenAPIGenerator + compat post-processing), filtered to operationId-bearing procedures
MCP (/api/mcp) hand-written tools (an early revision of this PR carried 37 of them) autogenerated from the router contract: one tool per published OpenAPI operation (39), executed through the same middleware chain via call()

One router, conditional auth

Every procedure runs on the same base middleware (server/orpc/base.ts):

if (request.headers.get("Authorization")) {
  // org API-key auth — byte-parity port of the legacy withApiHandler:
  // bearer parsing, cached key lookup, per-org rate limit (+ headers),
  // x-tenant-id validation, analytics identity
} else {
  // better-auth session (auth.api.getSession) + x-organization-id header,
  // membership-checked and hydrated to the same organization context
}
  • Shared procedures (everything with .route() metadata) serve REST, MCP, and the dashboard. requireRoles("admin", "owner") gates session members on the mutations the dashboard previously restricted (namespace create/delete, API keys, webhook mutations); API keys retain full org authority, matching the existing public-API semantics.
  • Dashboard-only procedures (slug-addressed lookups, richer table projections, billing, onboarding, org bootstrap) live in the same router on a session-only base and carry no route metadata — they are structurally excluded from REST routing, the generated spec, and MCP. They are thin data projections, not duplicated business logic: every mutation and agent-facing operation exists exactly once.
  • The dashboard client passes the active org as a per-call header (x-organization-id). oRPC's batch format preserves per-item headers, so batched calls authenticate independently.

Wire compatibility is byte-exact. The legacy {success, data, pagination?} envelopes, {success:false, error:{...}} error bodies (incl. the zod-error 422 format and the doc_url anchor quirk), status codes, hidden PUT aliases, rate-limit headers, x-tenant-id scoping, and cursor pagination are all preserved. Chat with stream: true returns the exact AI-SDK SSE stream via oRPC's detailed-output ReadableStream passthrough.

The generated OpenAPI document is byte-identical to the previous revision's (which was itself canonically identical to the legacy zod-openapi document) — re-verified after the consolidation, so Speakeasy and Mintlify are unaffected.

New REST endpoints (org API-key auth, OpenAPI-registered)

Area Endpoints
Organization GET/PATCH /v1/organization, GET /v1/organization/members
API keys GET/POST /v1/api-keys, DELETE /v1/api-keys/{keyId} (headless rotation)
Webhooks GET/POST /v1/webhooks, GET/PATCH/DELETE /v1/webhooks/{webhookId}, POST .../regenerate-secret
Chat POST /v1/namespace/{namespaceId}/chat — playground parity (normal/agentic/deepResearch), JSON default, stream: true for AI-SDK-compatible SSE
Custom domains GET/POST/DELETE /v1/namespace/{namespaceId}/hosting/domain

Hosted MCP server

https://api.agentset.ai/mcp (streamable HTTP via mcp-handler, SSE disabled) — Authorization: Bearer agentset_…, optional x-tenant-id. Tools are generated from the router contract (lib/mcp/index.ts, ~200 lines replacing ~1,600 lines of hand-written tools): one tool per published operation — 39 total — with names derived from operationIds (list_namespaces, create_ingest_job, …), titles/descriptions from route metadata, and readOnlyHint/destructiveHint/idempotentHint annotations from the HTTP method. Tool calls execute via oRPC call() through the full middleware chain, so auth, rate limiting, tenant scoping, input validation, and error mapping are REST-identical by construction (outputs are the REST envelope; chat pins stream: false). The stdio @agentset/mcp package remains the local option.

Parity & security fixes shipped alongside

  • tRPC/REST/MCP now share one router and service layer; fixes divergences found in the audit: REST POST /v1/namespace silently dropped embeddingConfig/vectorStoreConfig and skipped the totalNamespaces counter; REST re-ingest never emitted ingest_job.queued_for_resync; namespace list returned DELETING rows; PATCH /v1/namespace served stale cache up to 5 min; DELETEs returned invalid 204-with-body.
  • Fixed a warm-cache 500 on GET /v1/namespace/{namespaceId}: unstable_cache JSON-serializes the namespace row, so cache hits revived DateTime columns as strings and failed the z.date() output schema.
  • Fixed a pre-existing IDOR in the dashboard deleteApiKey (deleted by id without org check) — now enforced by the shared procedure's org scoping.
  • Disabled better-auth's stock POST /api/auth/organization/delete, which hard-deleted orgs bypassing Stripe/namespace cleanup (dashboard uses the service path).
  • Document get/delete/download-URLs now honor x-tenant-id scoping on REST and MCP (previously only list did).
  • Dashboard mutations surface typed 4xx errors instead of 500 (AgentsetApiErrorORPCError mapping at the RPC handler).
  • Hosting deletion removes the attached Vercel domain best-effort (fixes domain orphaning without blocking on the Vercel API).

Docs

17 new OpenAPI-driven reference pages, hosted-first MCP page (Claude Code / Claude Desktop / Cursor setup + tool table), new agent-first concept page. mintlify broken-links clean. Pages render once the Speakeasy-hosted spec re-ingests /openapi.json (now generated; canonically identical to the previous document).

Verification

  • Generated OpenAPI document diffed byte-identical against the pre-consolidation output; typecheck/eslint at the known baseline (zero net-new issues); vitest suite passes.
  • Runtime round-trips against a local stack over REST (auth/401/400/422 legacy bodies, rate-limit headers, org + namespace reads, real vector search, chat), the dashboard RPC surface (session + org-header path, missing-header 400, keyless 401, batched calls carrying per-item org headers), and MCP (initialize, 39 generated tools, live search/chat/get_namespace calls incl. error paths).
  • Browser end-to-end over the real batched client: documents tables, API-key create/delete (shared mutations + invalidations), hosting page (shared read + envelope-aware optimistic cache), playground search, webhooks — all passing with zero API errors.

tifa2UP added 2 commits July 2, 2026 14:03
… server

Add REST endpoints for organization (get/update/members), API keys
(list/create/delete), webhooks CRUD + regenerate-secret, RAG chat with
playground parity (normal/agentic/deepResearch, JSON or SSE), and custom
domain management. Add a hosted streamable-HTTP MCP server at /api/mcp
(mcp-handler) with 37 tools mirroring the v1 surface, authenticated with
org API keys.

Unify tRPC/REST/MCP onto one service layer, fixing parity divergences:
REST namespace create dropped embedding/vector-store configs, re-ingest
skipped its webhook event, deleted namespaces served from cache, invalid
204-with-body responses. Security: fix api-key delete IDOR, disable
better-auth stock org deletion (bypassed cleanup), honor x-tenant-id in
document get/delete/download-urls. Route protectedProcedure through the
AgentsetApiError conversion so dashboard mutations surface typed errors
instead of 500s.
Add OpenAPI-driven reference pages for the new organization, api-keys,
webhooks, chat, hosting-domain, and warm-up endpoints. Rewrite the MCP
server page hosted-first with client setup for Claude Code, Claude
Desktop, and Cursor plus a complete tool table. Add an agent-first
concept page.
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (214 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@mintlify

mintlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
agentset 🟢 Ready View Preview Jul 2, 2026, 12:04 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app-agentset-ai Ready Ready Preview, Comment Jul 6, 2026 11:08am

Request Review

tifa2UP added 5 commits July 4, 2026 11:06
…g, handler mounts

Public v1 catch-all serves an OpenAPIHandler with the exact legacy success/error
envelopes, rate-limit headers, and analytics logging; internal RPC handler
replaces the tRPC transport. Organization resource migrated as the exemplar
(GET/PATCH/PUT + members) with Speakeasy spec metadata carried on .route().
Legacy organization route files removed (catch-all now serves those paths).
All 21 remaining v1 endpoints are now oRPC procedures behind the catch-all
handler (wire-parity: envelopes, status codes, 204s, PUT aliases, pagination,
tenant scoping, chat SSE via detailed-output stream passthrough), with the
Speakeasy spec metadata carried on .route(). Legacy v1 route files removed.
All 52 tRPC procedures ported 1:1 to oRPC internal routers on the same
service layer; tRPC files remain until the client migration lands.
All 48 consumer files swap useTRPC()/trpcClient for the module-level orpc
utils and imperative client (query keys, partial-match invalidations,
optimistic setQueryData writes, isMutating filters, and RouterOutputs types
all derived from the oRPC utils). providers.tsx now mounts a plain
QueryClientProvider; superjson and the @trpc/* dependencies are gone.
buildOpenApiDocument() (OpenAPIGenerator + ZodToJsonSchemaConverter with
compat interceptors + deterministic post-processing) reproduces the legacy
zod-openapi document exactly — canonical JSON of old and new specs is
byte-identical across all 39 operations and 82 components, so the
Speakeasy-generated SDK and Mintlify docs are unaffected. The hand-written
spec tree (src/openapi, 58 files) is deleted; shared path/header param
schemas move to schemas/api/params.ts and zod-openapi is dropped from the
app. Also fixes a handful of new lint errors from the migration waves.
withApiHandler/withNamespaceApiHandler are fully replaced by the oRPC
middleware; only HandlerParams (shared by the session/public wrappers still
used by the internal chat and hosting routes) and the cached getNamespace
lookup (shared with MCP) remain.
@tifa2UP tifa2UP changed the title Agent-first: full REST API + hosted MCP coverage of the dashboard surface Agent-first: REST + MCP coverage of the dashboard surface, both APIs on oRPC Jul 4, 2026
Resolves the agentic-search-playground stack (#135/#138/#141) and the
free-plan upload limit (#136) against the oRPC migration:

- Legacy v1 upload routes and the tRPC billing router were deleted by the
  migration; main's changes are ported into their oRPC replacements
  (plan-aware createUpload/createBatchUpload with the file_too_large
  mapping on REST/RPC/MCP; getTrackedPages removed everywhere).
- The playground chat route/schema take main's newer agentic-search
  implementation; lib/chat (behind the public v1 chat and MCP) keeps the
  normal/agentic/deepResearch contract, restoring lib/agentic's pipeline
  entry, lib/deep-research, and the condense prompts that main dropped as
  dead code, adapted to the NamespaceLanguageModel bundle API from the
  LLM-getter consolidation.
- Generated OpenAPI spec re-verified against the pre-merge snapshot: the
  only diffs are main's intended ones (gpt-5.5 default, Cohere reranker
  default, upload-limit descriptions).
The generic pattern makes the local-only convention self-enforcing: any
file or directory named *.local is ignored without needing a dedicated
gitignore entry.
Addresses review feedback: the three parallel implementations (internal
oRPC router, public oRPC router, hand-written MCP tools) are now a single
router with a conditional auth middleware.

- server/orpc/base.ts: one auth middleware — if an Authorization header is
  present, org API-key auth (wire-parity port: bearer parse, cached key
  lookup, per-org rate limit + headers, x-tenant-id, analytics); otherwise
  better-auth getSession() + x-organization-id header, membership-checked
  and hydrated to the same organization context. requireRoles() gates
  session members on mutations the dashboard previously restricted to
  admin/owner; API keys keep full org authority (public-API parity).
- server/orpc/router/*: the single router, served by all three surfaces.
  Dashboard-only procedures (slug-addressed lookups, richer projections,
  billing, onboarding) stay session-gated and carry no route metadata, so
  they are excluded from REST routing, the generated spec, and MCP.
- lib/mcp: the 37 hand-written tools are replaced by a generator that walks
  the router contract and registers one tool per published operation (39),
  with titles/descriptions/read-only/destructive hints from route metadata,
  executing via call() through the full middleware chain — REST/MCP parity
  by construction.
- lib/orpc.ts + call sites: dashboard mutations moved onto the shared
  procedures with a per-call org header (batch-safe: oRPC preserves
  per-item headers; RequestHeadersPlugin restores them per batched call).
- lib/api/handler/namespace.ts: fix pre-existing warm-cache 500 on
  GET /v1/namespace/{namespaceId} — unstable_cache revives DateTime
  columns as strings, failing the z.date() output schema.

The generated OpenAPI document is byte-identical to the previous one
(verified), typecheck/lint/tests are at baseline, and all three surfaces
plus five dashboard flows were smoke-tested end-to-end.
@tifa2UP

tifa2UP commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

Addressed in 4c3e13e — the three implementations are now one oRPC router with the conditional auth check, and MCP is generated from the API contract.

One router. server/orpc/internal/* and server/orpc/public/* are merged into server/orpc/router/*, mounted by all three handlers (dashboard RPC, REST v1, MCP). The auth middleware in base.ts is exactly the shape you sketched:

if (apiKeyHeader) {
  // authenticate via api key (rate limit, tenant, analytics — unchanged wire behavior)
} else {
  // authenticate via better-auth session + x-organization-id header (membership + role checks)
}

Dashboard mutations now call the same procedures the public API serves (with requireRoles restoring the admin/owner gates the old internal router had — API keys keep full org authority as before). The handful of dashboard-only procedures that remain (slug-based lookups for URL routing, table projections that need fields the public schemas don't expose, billing, onboarding) are session-gated members of the same router with no route metadata, so they can't leak onto REST/OpenAPI/MCP; they're thin selects, not duplicated business logic.

MCP autogenerated. lib/mcp/tools/* (~1,600 lines) is replaced by a ~200-line generator that traverses the router contract — the same source the OpenAPI document is generated from — and registers one tool per published operation (39), executing through the full middleware chain via call(). Names, descriptions, input schemas, and read-only/destructive annotations all derive from the route metadata, so REST/MCP can't drift.

Net: −1,700 lines, and the generated /openapi.json re-verified byte-identical, so the Speakeasy/Mintlify pipeline is unaffected. Verification details are in the updated PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant