Imported from rociiu/mcploom (
AGENTS.md). Install upstream withnpx skills add rociiu/mcploom. Copyright stays with the author.
AGENTS.md
Guidance for AI agents (and humans) working in the MCPLoom codebase. Read this before making changes. It captures the commands, architecture, invariants, and non-obvious decisions that aren't apparent from any single file.
What MCPLoom is
MCPLoom turns a REST API (described by OpenAPI/Swagger) into a hosted, multi-tenant
MCP (Model Context Protocol) endpoint. A single Go process serves both the
dashboard JSON API and the public MCP endpoint for every workspace — there is
no per-customer MCP server. See README.md and docs/architecture.md.
Commands
All commands run from the repo root unless noted. make help lists targets.
Backend (Go)
make build # build ./bin/mcploom-api
make run # run the API (:8080); applies migrations on startup
make test # unit tests — NO database required
make vet # go vet ./...
make gen-key # print a fresh AES-256 master key
go build ./... # compile everything
gofmt -w . # format (run before finishing; CI-equivalent)
Integration tests (require Postgres)
Integration tests live in internal/server/server_integration_test.go and are
skipped unless MCPLOOM_TEST_DATABASE_URL is set. They DROP and recreate the
schema, so always point them at a throwaway database.
createdb mcploom_test # once
MCPLOOM_TEST_DATABASE_URL="postgres://$(whoami)@localhost:5432/mcploom_test?sslmode=disable" \
go test ./... -count=1
Frontend (Next.js, in apps/web/)
cd apps/web
npm install
npm run dev # dev server :3000
npm run typecheck # tsc --noEmit
npm run build # production build (also typechecks)
Full stack
cp .env.example .env && make gen-key # paste key into MCPLOOM_MASTER_KEY
docker compose up --build # frontend, api, postgres, redis
Definition of done for a change
Before considering work complete:
gofmt -w .(the linter reformatsvarblocks, struct tags, maps — match it).make vetclean.go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...clean (CI enforces it).make testgreen; if you touched HTTP/store/runtime, also run the integration tests.- If you touched the frontend:
npm testandnpm run buildgreen (build typechecks too).
Repository layout
apps/
api/main.go entrypoint + dependency wiring (DI). Subcommand: `gen-key`.
web/ Next.js 16 dashboard (App Router, Tailwind v4, TanStack Query)
internal/
domain/ entities; depends on NOTHING else. Start here to understand the model.
connector/ Parser interface + ToolBlueprint/Spec + Registry (the extension seam)
openapi/ OpenAPI 3.x / Swagger 2.0 parser + tool naming (implements connector.Parser)
auth/ AuthProvider strategy + 6 providers (outbound auth for upstream APIs)
secrets/ AES-256-GCM Cipher
mcp/ executor.go (tool→HTTP) + runtime.go (multi-tenant mcp-go servers)
store/ Postgres (pgx) persistence; implements mcp.ToolSource; migrate.go
config/ env config
server/ Gin handlers + middleware (auth.go has JWT + workspace scoping)
migrations/ *.sql + embed.go (embedded for the auto-migrator)
deployments/docker Dockerfile.api, Dockerfile.web
docs/ architecture, security-model, auth-providers, etc.
examples/ sample OpenAPI spec
Dependency direction points inward: domain knows nothing about HTTP/SQL/MCP.
Architecture invariants — do not break these
-
Tenancy is structural. Every workspace-owned query in
internal/storetakes aworkspaceIDand filters on it (WHERE ... AND workspace_id = $n). There is intentionally no unscoped "get by id". The dashboard API enforces membership inrequireWorkspacemiddleware (internal/server/auth.go), which returns 404 (not 403) for non-members so workspace existence isn't leaked.TestCrossTenantIsolationguards this — keep it passing. -
Secrets never leave the server. Auth config is encrypted with
secrets.Cipherbefore hitting Postgres (auth_profiles.config_encrypted). Decryption happens ONLY instore.getAuthProfileDecrypted/store.ResolveExecution. The HTTP API exposes the auth type, never the config.domainstructs that hold secrets usejson:"-". Never log secrets. The executor redactsAuthorization/Cookie/Proxy-Authorizationin traces. -
Source-agnostic tools. The runtime and storage only ever handle
connector.ToolBlueprint— they don't know what OpenAPI is. New upstream types (GraphQL/gRPC/SOAP) must implementconnector.Parserand register inapps/api/main.go; no other layer should change. -
Runtime cache invalidation.
mcp.Runtimecaches onemcp-goserver per workspace. ANY mutation to tools/auth/integration MUST callruntime.Invalidate(workspaceID)or the live MCP endpoint serves stale tools. Grep existing handlers (handleImportSpec,handleConfigureAuth,handleUpdateTool) for the pattern.
Non-obvious decisions (so you don't "fix" them)
- Custom OpenAPI parser, by design.
internal/openapidecodes specs into generic maps (viasigs.k8s.io/yaml→ JSON) and resolves$refby JSON pointer, handling both 3.x and 2.0 in one path. This was chosen over a heavy library so the parser is fully testable offline. Don't swap in a library without good reason. - Two migration strategies, pick one per deployment.
store.Migrateis an embedded auto-migrator (runs on startup, tracksmcploom_schema_migrations). Thegolang-migrateCLI (make migrate-up) is the alternative for out-of-band management; it uses its ownschema_migrationstable. They're independent. - Go 1.25 is required (the
mcp-godependency needs it;go.modsays1.25). - MCP streamable HTTP runs stateless (
server.WithStateLess(true)), so the gateway scales horizontally and can mount under/mcp/:slug— the handler dispatches purely on HTTP method, ignoring the path. - MCP endpoint authN is layered. The
/mcp/{slug}surface is gated by the unguessable slug + the endpointenabledflag, then by credentials: per-endpoint API keys (hashed, shown once) and — whenMCPLOOM_MCP_OAUTH_ISSUERis set — OAuth 2.1 Bearer JWTs validated against an external authorization server (internal/mcpauth), with RFC 9728 protected-resource metadata at/.well-known/oauth-protected-resource/mcp/:slug. Seedocs/security-model.md. - Redis backs rate limiting (
internal/ratelimit) when reachable; without it the limiter falls back to in-memory, so single-node needs no Redis.
How to extend
Add an outbound auth provider
- Implement
AuthProviderininternal/auth(a type withApply(ctx, *http.Request) error). - Add a case to
auth.Buildand a constant todomain.AuthType. - Add a table-driven test in
internal/auth/auth_test.go. The executor/runtime need no changes — they only hold the interface.
Add a source connector (GraphQL, gRPC, …)
- Implement
connector.Parser(SourceType()+Parse()→*connector.Spec). - Register it in
apps/api/main.go(parsers.Register(...)). - Add a
domain.SourceTypeconstant. Storage, runtime, and dashboard are already source-agnostic.
Add a migration
Create migrations/00000N_name.up.sql and .down.sql. The embedded migrator
picks up new *.up.sql files in version order on next start.
Add an API endpoint
Register the route in internal/server/server.go (routes()), put the handler
in the matching handlers_*.go. Workspace-scoped routes go under the ws group
so requireWorkspace runs first; read the validated id via currentWorkspace(c).
Frontend conventions
- All API access goes through the typed client in
apps/web/lib/api.ts. Don't callfetchdirectly from components. - Auth/workspace state lives in
apps/web/lib/session.tsx(useSession). - UI primitives are in
apps/web/components/ui.tsx(shadcn-style). Dark mode is class-based vianext-themes; colors are CSS variables defined inapp/globals.css(Tailwind v4@theme). - Server data fetching uses TanStack Query; after a mutation that affects the MCP server, the backend invalidates its own cache — the frontend just refetches.
Gotchas
- Don't commit
.envor any realMCPLOOM_MASTER_KEY/MCPLOOM_JWT_SECRET. - The API refuses to start without
MCPLOOM_MASTER_KEY;MCPLOOM_JWT_SECRETis required only in production (MCPLOOM_ENV=production). apps/web/node_modulesand.nextare gitignored;next-env.d.tsandtsconfig.jsonare auto-modified by Next during build — that's expected.- Tool names must follow the verb rules (
get_/create_/update_/patch_/delete_); seeinternal/openapi/naming.goand its tests before changing.