Skip to content

feat(docs-mcp): local/offline stdio MCP server for docs search#442

Open
rejifald wants to merge 2 commits into
mainfrom
claude/elastic-bhaskara-ca4516
Open

feat(docs-mcp): local/offline stdio MCP server for docs search#442
rejifald wants to merge 2 commits into
mainfrom
claude/elastic-bhaskara-ca4516

Conversation

@rejifald

@rejifald rejifald commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Adds @stitchapi/docs-mcp — a local, offline stdio counterpart to the hosted docs-search MCP server at stitchapi.dev/api/mcp. Same search_docs/get_doc contract, for the cases where a hosted third-party dependency is a dealbreaker: air-gapped/strict-egress environments, or just no per-query network call.

  • Docs content is bundled at build time, not fetched at runtime. A new, purely-additive apps/docs/scripts/build-docs-pages.ts (mirrors the existing build-search-index.ts pipeline) plus packages/docs-mcp/scripts/copy-bundle.mjs produce the package's data/ snapshot from the real apps/docs content pipeline — no separate publish cadence, no live network dependency. Freshness rides the repo's existing lockstep release train (npm-publish.yml already builds every packages/* package before publishing), so no new CI workflow was needed.
  • Query embedding runs locally via transformers.js (same model as the hosted route), cached to a persistent OS cache dir instead of a per-cold-start /tmp — downloads once, then fully offline.
  • A Dockerfile builds from the monorepo source (not a published npm version), giving Glama's Dockerfile-based MCP catalog check something real to introspect.
  • src/config.ts/doc-path.ts are deliberate, flagged mirrors of apps/docs/lib/search-index/* (same hand-mirror tradeoff @stitchapi/aws-sigv4 already documents for its formEncode helper) — see the package README's "Keeping this in sync".

This diff went through a 5-lens adversarial review (security, correctness/parity with the hosted server, reliability, test coverage, packaging/release) before opening — security and correctness/parity came back clean; reliability found two "cache a rejected promise forever" bugs in the long-lived stdio process (fixed: getEmbedder()/loadIndex() now reset and retry after a transient failure instead of wedging every later search_docs call), plus a missing prepack script (fixed — npm pack/publish was silently shipping an empty tarball) and an opaque error message in the build script (fixed). All test-coverage gaps the review found were also closed.

Test plan

  • pnpm --filter @stitchapi/docs-mcp test — 42 tests across 8 files (doc-path parsing, query-cap DoS guard, hit-mapping shape, MCP tool contract end-to-end via InMemoryTransport, error-recovery/retry behavior, CLI --help/--version, barrel exports)
  • pnpm --filter @stitchapi/docs-mcp check:types
  • Full real end-to-end smoke test: ran the actual apps/docs build pipeline (583 chunks, 109 pages, real model download), copied it into the package, and queried it — search_docs("how do I retry a failed request") correctly ranked the Retry & backoff page first (0.916), get_doc({slug: "guides/resilience/throttle"}) returned the real page content
  • node scripts/check-release.mjs (lockstep version/license/engines, LICENSE+README presence)
  • Full pre-push gate (format, lint, contract, typecheck, typecheck-d, test, exports, build-docs, yakir) green
  • Manual: point an MCP client (e.g. Claude Desktop) at npx @stitchapi/docs-mcp after this is published, and confirm the real npm tarball installs and runs correctly

🤖 Generated with Claude Code

Adds @stitchapi/docs-mcp — a stdio counterpart to the hosted stitchapi.dev/api/mcp
docs-search server, for users who need zero per-query network calls (air-gapped or
strict-egress environments, or just no third-party dependency). Same search_docs/
get_doc contract, verified end-to-end against a real build of the docs corpus.

Docs content is bundled at build time (not fetched at runtime) via a new,
purely-additive apps/docs/scripts/build-docs-pages.ts script plus
scripts/copy-bundle.mjs, so freshness rides the existing lockstep release train
(no new publish workflow needed) and the package stays reproducible/auditable per
version. A Dockerfile builds from the monorepo source, giving Glama's Dockerfile-
based catalog check something real to introspect.
// Local-open embedding model (transformers.js) — must match
// apps/docs/lib/search-index/config.ts EMBED_MODEL exactly, or a query embeds
// into a different vector space than the bundled index was built in.
export const EMBED_MODEL = 'Xenova/all-MiniLM-L6-v2';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to be in the shared config or global env variable so it stays always in sync and won't drift.

return flat.length > EXCERPT_LEN ? `${flat.slice(0, EXCERPT_LEN)}…` : flat;
}

function absoluteUrl(pageUrl: string, anchor: string): string {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have function that does it in the core, we should reuse it

const SITE_URL = 'https://stitchapi.dev';
const EXCERPT_LEN = 300;

function excerpt(text: string): string {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably in the core, reuse

},
);

server.tool(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a stitch ?

# Build (from repo root): docker build -f packages/docs-mcp/Dockerfile -t stitchapi-docs-mcp .
# Run: docker run -i stitchapi-docs-mcp

FROM node:24-slim AS builder

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock version number for deterministic builds

…/doc-path

The docs-mcp README already flagged config.ts/doc-path.ts/SITE_URL/EXCERPT_LEN as
deliberate hand-mirrors of apps/docs's search-index code — but that was only a
comment. Adds scripts/probe-docs-mcp-parity.mjs (imports both sides' config
constants and runs parseDocPath against a fixed input table) and a new
docs-mcp-config-parity yakir tether comparing the two sides' output, so a real
divergence now fails CI instead of silently shipping.
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