[MCP] foundation: component scaffold + config schema + boot gating#649
Conversation
Lands the empty MCP component, the mcp: config block, and a config-gated registration hook in the operations server. Until #614 ships the Streamable HTTP transport, flipping mcp.operations.enabled: true serves a 503 with `{error:"mcp_not_implemented", profile:"operations"}` from the configured mountPath — enough to prove the gate end-to-end. The application-profile call site is intentionally deferred to #614, where the transport will register itself via server.http(...). The shared registerMcpProfile() supports both profiles already (verified by tests). Defaults: mcp.{operations,application}.enabled: false. Existing deployments see no change on upgrade. Closes #613 Tracking: #465 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Reviewed; no blockers found. |
Gemini cross-model review surfaced two issues: 1. **Truthiness regression.** `env.get(MCP_OPERATIONS_ENABLED)` can return the literal string `'false'` when sourced from an environment variable, which is truthy in JS — `mcp.operations.enabled: false` would still register the route. Switched to the in-file pattern used for `studioOn` (commonUtils.isEmpty + toString().toLowerCase() === 'true'). 2. **Auth missing on stub route.** The /mcp stub had no preValidation while the rest of the operations port uses authHandler. Pre-staging the design's auth-first stance from #465: registerMcpProfile now accepts optional `routeOptions` (forwarded as Fastify's 3-arg post), and the operations call site passes `{preValidation:[authHandler]}`. Also removed the redundant `?? '/mcp'` fallback at the call site — registerMcpProfile already defaults the mount path. New tests cover routeOptions forwarding and the strict-boolean contract. Refs #613. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cross-model review (Gemini 0.42.0) — appliedGemini surfaced one critical bug + two improvements. Pushed as a fix commit on top of the foundation. Critical: env-sourced
|
kriszyp
left a comment
There was a problem hiding this comment.
This looks like a good foundation. I generally have a preference towards cleaner and simpler default config files that are easy for users to get started with, but if you feel like this is helpful, that's fine.
| config: { | ||
| mcp: { | ||
| operations: { | ||
| enabled: true, |
There was a problem hiding this comment.
This pattern has always seemed a little verbose to me. Normally, we include the components/functionality in the config that we want enabled. Here we include operations to say that we maybe do or don't really want to include operations (depending on if it is enabled)? Rather than just including or omitting it? I guess maybe it is more explicit? IMHO, it seems like omitting operations from mcp is a pretty good way of omitting operations from MCP.
There was a problem hiding this comment.
Good feedback, will adjust.
| hostname: null | ||
| mcp: | ||
| operations: | ||
| enabled: false |
There was a problem hiding this comment.
So we are adding a bunch of default stuff to the config that isn't enabled? (my concern is there is already a lot to read in there that can confuse people)
There was a problem hiding this comment.
Got it, the preference is if enabled: false we do not add the remaining config? I sere the challenge that when the feature is turned on the admin would need to read docs to know how to config. Not arguing, just calling that out.
Please let me know how you would like this tightened up outside of the: and Instead there will be no |
Per Harper convention (matches `replication`): a profile is enabled iff its config sub-block is present, not via an `enabled: true/false` flag. - Drop `mcp.<profile>.enabled` from the Joi schema. - Drop the `mcp:` block from static/defaultConfig.yaml entirely — opt-in. - Drop `MCP_OPERATIONS_ENABLED` / `MCP_APPLICATION_ENABLED` config keys. - Component: skip registration when `config.mcp.<profile>` is absent. - Operations call site: gate on presence of `MCP_OPERATIONS_MOUNTPATH` (Joi always defaults `mountPath` to `/mcp` when the block is set). Backward compatibility: existing deployments have no `mcp:` block, so this preserves the "MCP off by default" behavior — same outcome as the prior `enabled: false` defaults, fewer keys to maintain. Refs #613. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Refactor: presence-based enablement (drop
|
Audit against Harper's config patterns surfaced a real bug: only six
hardcoded Joi defaults are re-applied to configDoc in
`config/configUtils.js:483-490`, so the Joi `.default('/mcp')` on
`mcp.operations.mountPath` never reaches `env.get(...)`. The previous
gate `env.get(MCP_OPERATIONS_MOUNTPATH)` would silently return undefined
when a user wrote `mcp: { operations: {} }`, leaving MCP off despite
configuration.
Switched the call site to:
- Import `getConfigObj` from `config/configUtils.js` (matches the
existing pattern in `bin/run.ts:355`).
- Gate on block presence: `if (fullConfig.mcp?.operations)`.
- Pass the full merged config object through to `registerMcpProfile`
instead of synthesizing a fake `{mcp:{...}}` from individual env keys.
Drops the boolean-string-coercion shim and the misleading "mountPath as
presence sentinel" comment — the component already falls back to
`DEFAULT_MOUNT_PATH = '/mcp'` when mountPath is absent on the nested
config.
Refs #613.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Config-pattern audit + fix (commit d5c8ea3)Audited the foundation against Harper's existing config conventions (read Real bug found and fixed
Fix: switched the gate to call Stylistic cleanups also addressed
Alignments verified correct, no change
Out of scope (Harper-wide, flagged for follow-up)The Joi-default propagation gap ( Verification
|
CI flake on Node.js v24 unit shard — re-run triggeredOne unit test failed on Node 24 only (Node 20 and 22 passed). The failure is in Not caused by this PR. This branch's diff is 6 files ( Likely cause: timing flake. The log shows Re-ran the failed job (run 26185818115). If it goes green on retry, the flake is confirmed. If it fails again, I'll dig deeper. |
First real `ModelBackend` against the Ollama HTTP API: `embed` via `/api/embed`, `generate` via `/api/generate` or `/api/chat`, and `generateStream` via NDJSON over chunked HTTP. Backend lives under `components/<name>/` matching the pattern from the MCP foundation (PR #649) — core imports `registerOllamaBackend` and calls it during boot; the file is not a `handleApplication(scope)` self-loader. Capabilities advertise `tools: false` and `adapters: false`. Ollama tool support exists on some models but is uneven across the catalog; the v1 portability guarantee keeps it off here. Validates the Phase 1 (#628 / PR #638) `ModelBackend` interface against a non-trivial real provider without external dependencies in CI. Tracking: #629, #510 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kriszyp
left a comment
There was a problem hiding this comment.
Awesome, thank you! Sorry for being nitpicky!
# Conflicts: # unitTests/validation/configValidator.test.js # validation/configValidator.ts
|
CI flake on merge commit Job: That test file is new on Other 34 checks: ✅. Rerunning just the failed Bun shard. |
Summary
Lands the foundation for the native MCP server design (#465) — the empty MCP component, the
mcp:config block, and a presence-gated registration hook in the operations server. Plumbing only. No JSON-RPC, no transport, no tools, no resources yet.Closes #613
Tracking: #465 (sub-issue #1 of 11)
What this PR lands
components/mcp/index.tsexportingregisterMcpProfile({ profile, host, config, routeOptions })— supports bothoperationsandapplicationprofiles via one entry point.validation/configValidator.tscovering both profiles andmcp.session. Noenabledflag — profile presence in config gates registration (matches Harper'sreplicationconvention).static/defaultConfig.yamlintentionally omits themcp:block (opt-in via block presence; same asreplication). Existing deployments see zero behavior change on upgrade.utility/hdbTerms.ts(MCP_*flat names; reserved for env-var overrides viaHARPER_MCP_OPERATIONS_MOUNTPATH=…etc., not used by the runtime gate itself).server/operationsServer.ts(afterregisterContentHandlers, before/health). UsesgetConfigObj()to read the merged config tree and gates onfullConfig.mcp?.operations. Auth wired viarouteOptions: { preValidation: [authHandler] }.registerMcpProfile+ stub handler (node:assert/strict, nosinon/rewire); 5 inconfigValidator.test.jsfor parse-clean / defaults / full-block / bad-types.What's still off / gated
mcp:block in config (the default),POST <opsPort>/mcpreturns 404 — unchanged behavior.mcp: { operations: {} }in config,POST <opsPort>/mcpreturns 503 with body{"error":"mcp_not_implemented","profile":"operations"}. This is the placeholder; [MCP] Streamable HTTP transport: session, lifecycle, Origin, contentTypes reuse #614 swaps the body for the real Streamable HTTP transport without changing the gate.registerMcpProfile()already supports both profiles (verified by tests with a fake host), but there is no clean global startup site to wire the HTTP-port listener for a 503 stub. [MCP] Streamable HTTP transport: session, lifecycle, Origin, contentTypes reuse #614 brings that wiring in alongside the real transport.This trade-off is called out explicitly on #465; the acceptance criterion "registration hook usable by both servers" is satisfied — the hook is in place, only the application boot invocation moves.
Where to put attention
server/operationsServer.ts— the gate usesgetConfigObj()andif (fullConfig.mcp?.operations). Block-level presence is intentional because Joi.default(...)values undermcp.*are not propagated back intoenv.get(config/configUtils.js:483-490only re-applies six specific defaults; flagged as a Harper-wide cleanup, not blocking).components/mcp/index.ts:FastifyLike— the loosepost: (path: string, ...rest: unknown[]) => unknownshape avoids pulling Fastify types into the component (per EVP review point Add some unit tests #9 on Expose Operations (API) through an MCP Wrapper #465 — no new Fastify API surface).validation/configValidator.ts— the application profile inherits from the operations schema via.keys({ searchMaxResults: … })to keep the shape declarative.Verification
Test plan
mcp:absent → 404;mcp.operations: {}→ 503 with the documented body.mcp:is absent.Notes
'false'string was truthy in the oldenabled-based gate) plus a defense-in-depth gap (stub/mcplackedauthHandlerpreValidation). Both fixed before the convention switch — see the disposition comment thread.enabledfield entirely; profiles are presence-gated now. Design doc on Expose Operations (API) through an MCP Wrapper #465 updated to match.mcpdon't propagate back toenv.get(configUtils.js:483-490re-applies only six specific defaults). Switched togetConfigObj()+ block-presence check; see the dedicated comment thread.