Skip to content

Commit 57b89b4

Browse files
os-zhuangclaude
andauthored
feat(mcp): default-on MCP surface — core capability, opt out with OS_MCP_SERVER_ENABLED=false (#2698) (#2712)
MCP is a core platform capability: /api/v1/mcp is now served (and advertised in /discovery) by default, and the OAuth 2.1 / DCR track follows it, so a fresh deployment is connectable by any MCP client with zero configuration. Single decision point: isMcpServerEnabled() in @objectstack/types — the dispatcher route gate, CLI plugin auto-load, REST /discovery advertisement, and auth-service OAuth/DCR follow-defaults all delegate to it. Explicit 'true' additionally auto-starts the stdio transport (unchanged, opt-in: a default must not claim the process's stdio); explicit 'false' turns the whole surface off fail-closed (404, no metadata, no DCR). Verified live on the showcase app: default boot serves MCP + PRM + DCR and 401s with the WWW-Authenticate challenge; OS_MCP_SERVER_ENABLED=false boot 404s all of them and drops the /discovery advertisement. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b271691 commit 57b89b4

22 files changed

Lines changed: 182 additions & 59 deletions

.changeset/mcp-default-on.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/types': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/rest': minor
5+
'@objectstack/cli': minor
6+
'@objectstack/mcp': minor
7+
'@objectstack/plugin-auth': minor
8+
---
9+
10+
feat(mcp): the MCP surface is now **default-on** — a core platform capability (#2698)
11+
12+
`/api/v1/mcp` is served (and advertised in `/discovery`) out of the box; the
13+
OAuth 2.1 authorization track and Dynamic Client Registration follow it, so a
14+
fresh deployment is connectable by any MCP client with zero configuration.
15+
Operators opt OUT with `OS_MCP_SERVER_ENABLED=false`.
16+
17+
- New single decision point `isMcpServerEnabled()` in `@objectstack/types`
18+
(default on; explicit `false`/`0`/`off`/`no` disables). The runtime
19+
dispatcher's `/mcp` route gate, the CLI's MCP plugin auto-load, the REST
20+
`/discovery` advertisement, and the auth service's OAuth/DCR follow-defaults
21+
all delegate to it — the served route, the advertised route, and the
22+
authorization track can never disagree.
23+
- The env var is now effectively tri-state: unset → HTTP surface on;
24+
explicit `true` → additionally auto-start the long-lived **stdio** transport
25+
at boot (unchanged, still opt-in — a default must not claim the process's
26+
stdin/stdout); explicit `false` → everything off, fail-closed (404, no
27+
metadata, no DCR).
28+
- The OAuth 2.1 TLS rule is unaffected: on a plain-HTTP non-loopback origin
29+
the OAuth track stays dark and the default-on surface remains API-key-only.

.changeset/mcp-oauth-authorization.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ decisions lives in #2698; the moving parts:
2626
the MCP spec requires) whenever the MCP surface is on — every deployment is a
2727
distinct AS, so clients cannot ship pre-registered IDs. Force it either way
2828
with `OS_OIDC_DCR_ENABLED` or the new `plugins.dynamicClientRegistration`
29-
auth-config field. The embedded AS itself now auto-enables when
30-
`OS_MCP_SERVER_ENABLED=true` (explicit `OS_OIDC_PROVIDER_ENABLED=false` still
31-
wins).
29+
auth-config field. The embedded AS itself auto-enables whenever the MCP
30+
surface is on — which is now the default (explicit
31+
`OS_OIDC_PROVIDER_ENABLED=false` still wins).
3232
- **Authorization-code + PKCE** flow with RFC 8707 resource binding: access
3333
tokens are minted with `aud=<origin>/api/v1/mcp` and verified locally
3434
(signature/issuer/audience/expiry) against the deployment's own JWKS —

content/docs/ai/agents.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ instead — see the callout in the [AI Overview](/docs/ai).)
4444

4545
## Connect your AI (BYO-AI over MCP)
4646

47-
Every deployment with `OS_MCP_SERVER_ENABLED=true` serves MCP at
48-
`/api/v1/mcp`, with two authentication tracks:
47+
Every deployment serves MCP at `/api/v1/mcp` by default (a core platform
48+
capability — set `OS_MCP_SERVER_ENABLED=false` to opt out), with two
49+
authentication tracks:
4950

5051
- **OAuth 2.1 (interactive clients — recommended).** Each deployment is its
5152
own spec-compliant authorization server: the endpoint publishes

content/docs/api/index.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ REST and GraphQL are how *code* consumes your app. **MCP is how *AI* consumes it
2929
Because every object and action is typed metadata, ObjectStack can expose the whole app
3030
as a [Model Context Protocol](https://modelcontextprotocol.io) server — so an AI client
3131
(Claude Code, Claude Desktop, Cursor, a local model) can inspect and *operate* the app
32-
you built, under the same permissions and RLS as the UI. Enable it with
33-
`OS_MCP_SERVER_ENABLED=true` (see [environment variables](/docs/deployment/environment-variables#mcp-server)).
32+
you built, under the same permissions and RLS as the UI. It is served at `/api/v1/mcp`
33+
by default — set `OS_MCP_SERVER_ENABLED=false` to opt out
34+
(see [environment variables](/docs/deployment/environment-variables#mcp-server)).
3435

3536
The generated tools mirror the surfaces you already defined:
3637
`list_objects` / `describe_object` (discover the schema), `query_records` / `get_record`

content/docs/deployment/environment-variables.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,14 @@ same RBAC / RLS as the Console. No custom tooling, no separate API. See
223223
[Actions as Tools](/docs/ai/actions-as-tools) for the tool set.
224224

225225
```bash
226-
OS_MCP_SERVER_ENABLED=true os start # boot with the MCP server (stdio by default)
226+
os start # MCP is served at /api/v1/mcp by default
227+
OS_MCP_SERVER_ENABLED=false os start # opt out of the MCP surface
228+
OS_MCP_SERVER_ENABLED=true os start # additionally auto-start the stdio transport
227229
```
228230

229231
| Variable | Type | Default | Description |
230232
|:---|:---|:---|:---|
231-
| `OS_MCP_SERVER_ENABLED` | boolean | `false` | When `true`, start the Model Context Protocol server at boot — exposing your objects and actions to MCP clients. |
233+
| `OS_MCP_SERVER_ENABLED` | boolean | `true` | The MCP HTTP surface (`/api/v1/mcp`) is a core capability and defaults **on**. Set `false` to disable it. An explicit `true` additionally auto-starts the long-lived stdio transport at boot. |
232234
| `OS_MCP_SERVER_NAME` | string | `objectstack` | Server name advertised to MCP clients. |
233235
| `OS_MCP_SERVER_TRANSPORT` | enum | `stdio` | `stdio` \| `http`. Use `http` (Streamable HTTP) for a remote client; `stdio` for a local one. |
234236

content/docs/deployment/index.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@ convention as `OS_MAIL_<KEY>` (e.g. `OS_MAIL_SMTP_HOST`, `OS_MAIL_SMTP_PORT`),
134134
not a raw `SMTP_*` name.
135135

136136
<Callout type="tip">
137-
**Make the deployed app AI-operable:** set `OS_MCP_SERVER_ENABLED=true` to start an MCP
138-
server alongside any deployment mode, exposing your objects and actions to AI clients
139-
under the same permissions and RLS. See
137+
**Every deployed app is AI-operable by default:** an MCP server is served at
138+
`/api/v1/mcp` in every deployment mode, exposing your objects and actions to AI clients
139+
under the same permissions and RLS (set `OS_MCP_SERVER_ENABLED=false` to opt out). See
140140
[MCP Server env vars](/docs/deployment/environment-variables#mcp-server) and
141141
[Your app as an MCP server](/docs/api#your-app-as-an-mcp-server).
142142
</Callout>

content/docs/getting-started/build-with-claude-code.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,8 @@ security apply to the agent exactly as they do to a person. The support desk you
309309
built in six steps is now a backend an agent can *run* — not just a database it
310310
can read.
311311

312-
Connecting is self-serve. Start the app with the MCP surface on
313-
(`OS_MCP_SERVER_ENABLED=true`), then add it to your client — the deployment is
312+
Connecting is self-serve. The MCP surface is on by default — just add the
313+
deployment to your client — and the deployment is
314314
its own OAuth 2.1 authorization server, so interactive clients just open a
315315
browser login (you connect as yourself, no admin-minted credentials):
316316

packages/cli/src/commands/serve.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import chalk from 'chalk';
88
import { bundleRequire } from 'bundle-require';
99
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
1010
import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js';
11-
import { readEnvWithDeprecation, resolveMultiOrgEnabled } from '@objectstack/types';
11+
import { readEnvWithDeprecation, resolveMultiOrgEnabled, isMcpServerEnabled } from '@objectstack/types';
1212
import { resolveObjectStackHome } from '@objectstack/runtime';
1313
import { LOG_LEVELS, resolveLogLevel, readLogLevelEnv } from '../utils/log-level.js';
1414
import {
@@ -474,12 +474,13 @@ export default class Serve extends Command {
474474
if (requires.includes('auth') && !requires.includes('email')) {
475475
requires.push('email');
476476
}
477-
// `OS_MCP_SERVER_ENABLED=true` says "serve MCP at /api/v1/mcp" — the
478-
// dispatcher gates the route on the SAME env var, so honoring the flag
479-
// without also loading the MCP plugin would 501 every request (#2698:
480-
// flipping one switch must yield a connectable MCP endpoint). Explicit
481-
// `requires: ['mcp']` in config works without the env var too.
482-
if (process.env.OS_MCP_SERVER_ENABLED === 'true' && !requires.includes('mcp')) {
477+
// MCP is a default-on core capability: serve `/api/v1/mcp` unless
478+
// `OS_MCP_SERVER_ENABLED=false` opts out. The dispatcher gates the route
479+
// on the SAME decision point (`isMcpServerEnabled`), so serving the
480+
// route without also loading the MCP plugin would 501 every request
481+
// (#2698: the default must yield a connectable MCP endpoint). Explicit
482+
// `requires: ['mcp']` in config works regardless of the env var.
483+
if (isMcpServerEnabled() && !requires.includes('mcp')) {
483484
requires.push('mcp');
484485
}
485486
// Default capability slate — every preset except `minimal` gets the

packages/mcp/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,9 @@ Please provide:
273273

274274
### Connecting to a running deployment (remote HTTP)
275275

276-
A running ObjectStack deployment (with `OS_MCP_SERVER_ENABLED=true`) serves
277-
MCP over Streamable HTTP at `/api/v1/mcp`. Two authentication tracks:
276+
A running ObjectStack deployment serves MCP over Streamable HTTP at
277+
`/api/v1/mcp` by default (set `OS_MCP_SERVER_ENABLED=false` to opt out). Two
278+
authentication tracks:
278279

279280
**OAuth 2.1 — the human-client track (recommended).** Each deployment is its
280281
own spec-compliant authorization server (backed by the embedded better-auth

packages/mcp/src/__tests__/plugin.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ describe('MCPServerPlugin', () => {
151151
await plugin.start(ctx as any);
152152

153153
expect(ctx.logger.info).toHaveBeenCalledWith(
154-
expect.stringContaining('[MCP] Server ready but not started'),
154+
expect.stringContaining('[MCP] Transport not auto-started'),
155155
);
156156
expect(ctx.trigger).toHaveBeenCalledWith('mcp:ready', expect.any(Object));
157157
});
@@ -204,7 +204,7 @@ describe('MCPServerPlugin', () => {
204204
await plugin.start(ctx as any);
205205

206206
expect(ctx.logger.info).toHaveBeenCalledWith(
207-
expect.stringContaining('[MCP] Server ready but not started'),
207+
expect.stringContaining('[MCP] Transport not auto-started'),
208208
);
209209
});
210210

0 commit comments

Comments
 (0)