Skip to content

Commit 48e8fa3

Browse files
committed
feat: default preset ships the three generic connector executors + live provider:'mcp' showcase demo (#3056)
Closes the last authoring gap in ADR-0097's 'expressible AND executable as pure metadata' promise, and lands the provider:'mcp' acceptance demo the ADR originally envisioned (deferred at #3017). create-objectstack (blank template): - plugins: gains new ConnectorRestPlugin() / ConnectorOpenApiPlugin() / ConnectorMcpPlugin() (zero-arg = provider factory only), so a scaffolded app materializes declarative provider-bound connectors with no plugin-wiring step. README documents the default providers, credentialRef rule, and the #3055 declarativeStdio opt-in. Deps added at ^15.0.0. examples/app-showcase (live mcp demo): - scripts/mcp-fixture.mjs — tiny in-repo stdio MCP server (one deterministic echo_upper tool): no network, no ports, no boot-ordering coupling, so the demo is CI-deterministic. - DevToolsMcpConnector (provider: 'mcp', stdio transport 'node ./scripts/mcp-fixture.mjs') + ShowcaseMcpConnectorEchoFlow dispatching echo_upper end-to-end; coverage notes updated. - objectstack.config.ts wires new ConnectorMcpPlugin({ declarativeStdio: ['node'] }) — dogfooding the #3055 default-deny opt-in, with the coarse-boundary caveat spelled out. dogfood: - showcase-declarative-mcp.dogfood.test.ts boots the REAL showcase (chdir to the app root, exactly how os dev runs), injects the three executors, and pins: GET /connectors lists showcase_mcp_tools origin=declarative state=ready with echo_upper (rest + openapi siblings ready too; catalog descriptor absent), and the flow trigger round-trips OBJECTSTACK from the fixture's tools/call into the run output. docs: flows.mdx explains where connector_action connectors come from, the default preset, and the stdio opt-in. Verified: dogfood proof 2/2; turbo test dogfood+showcase+create-objectstack 63 tasks green; showcase validate (23 flows / 8 plugins) + typecheck clean; eslint clean; lockfile updated for the new deps. Refs #3056 (template + showcase + demo + docs; issue also tracks nothing further after this — see closing comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pbmw3pMqNJfPbkvwh9Fkjs
1 parent 5a97012 commit 48e8fa3

14 files changed

Lines changed: 338 additions & 2 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'create-objectstack': minor
3+
---
4+
5+
feat(create-objectstack): scaffold ships the three generic connector executors (#3056)
6+
7+
The blank template's `plugins:` now includes `new ConnectorRestPlugin()`,
8+
`new ConnectorOpenApiPlugin()`, and `new ConnectorMcpPlugin()` (zero-arg =
9+
provider factory only, no hand-wired connector), so a scaffolded app can
10+
declare provider-bound `connectors:` entries (ADR-0097) — `provider: 'rest' |
11+
'openapi' | 'mcp'` — as pure metadata and have them materialize into live,
12+
dispatchable connectors at boot, with no plugin-wiring step. The template
13+
README documents the default providers, the `credentialRef` rule, and the
14+
#3055 `declarativeStdio` opt-in (declarative stdio transports stay denied by
15+
default). Remove unwanted executors by deleting a line from `plugins:`.

content/docs/automation/flows.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ Each node performs a specific action in the flow.
117117
| `map` | Sequential multi-instance — run a per-item subflow for each element, one at a time (each may pause); batch approval (ADR-0039) |
118118
| `connector_action` | Execute an external connector action |
119119

120+
`connector_action` dispatches against the runtime **connector registry**, which connector plugins populate. Scaffolded apps ship the three **generic executors**`rest`, `openapi`, and `mcp` — in their default `plugins:` (remove any you don't want), so a declarative `connectors:` entry that names a `provider` is materialized into a live, dispatchable connector at boot with no plugin code (ADR-0097). Brand connectors (Slack, …) are installed separately. One security note: a declarative `mcp` instance using a **stdio** transport spawns a local process from metadata and is therefore denied by default — the host opts in with `new ConnectorMcpPlugin({ declarativeStdio: ['<trusted-command>'] })`; `http` transports need no opt-in.
121+
120122
### Node Structure
121123

122124
```typescript

examples/app-showcase/objectstack.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { defineStack } from '@objectstack/spec';
4+
import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
45
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
56
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
67
import { ConnectorSlackPlugin } from '@objectstack/connector-slack';
@@ -112,8 +113,19 @@ export default defineStack({
112113
// (ADR-0097), which materializes the StatusOpenApiConnector
113114
// declarative instance below — its OpenAPI document is a
114115
// package-relative FILE PATH read at boot (#3016).
116+
// • mcp → contributes the `mcp` provider factory, which materializes
117+
// the DevToolsMcpConnector declarative instance from the
118+
// in-repo stdio fixture (scripts/mcp-fixture.mjs). The
119+
// `declarativeStdio` allowlist is the #3055 security opt-in:
120+
// declarative stdio transports spawn a local process from
121+
// METADATA, so they are denied by default; this host
122+
// deliberately trusts `node` to run the in-repo fixture.
123+
// (A coarse boundary by design — trusting `node` trusts what
124+
// it is asked to run; real deployments should list specific
125+
// server binaries.)
115126
plugins: [
116127
new ConnectorOpenApiPlugin(),
128+
new ConnectorMcpPlugin({ declarativeStdio: ['node'] }),
117129
new ConnectorRestPlugin({
118130
name: 'rest',
119131
baseUrl: process.env.SHOWCASE_SELF_URL ?? 'http://127.0.0.1:3000',

examples/app-showcase/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
"test:smoke": "playwright test --config=playwright.config.ts"
2323
},
2424
"dependencies": {
25+
"@modelcontextprotocol/sdk": "^1.29.0",
2526
"@objectstack/cloud-connection": "workspace:*",
27+
"@objectstack/connector-mcp": "workspace:*",
2628
"@objectstack/connector-openapi": "workspace:*",
2729
"@objectstack/connector-rest": "workspace:*",
2830
"@objectstack/connector-slack": "workspace:*",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Tiny in-repo MCP server (stdio) — the deterministic target for the
4+
// showcase's declarative `provider: 'mcp'` connector instance (#3056,
5+
// completing the live demo deferred from #3017 / ADR-0097 §6).
6+
//
7+
// Why a fixture instead of a real server: the demo must materialize during
8+
// BOOT in CI (the Dogfood Regression Gate), so the target has to exist with
9+
// no network, no ports, and no boot-ordering coupling. A stdio child process
10+
// spawned at materialization is exactly that. The spawn is allowlisted by the
11+
// host via `new ConnectorMcpPlugin({ declarativeStdio: ['node'] })` in
12+
// objectstack.config.ts — dogfooding the #3055 opt-in.
13+
//
14+
// One deliberately boring, deterministic tool: `echo_upper` upper-cases the
15+
// input text, so the flow run's captured output proves the whole chain
16+
// (metadata entry → mcp provider factory → tools/list → connector_action
17+
// dispatch → tools/call) with zero flakiness.
18+
19+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
20+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
22+
23+
const server = new Server(
24+
{ name: 'showcase-mcp-fixture', version: '1.0.0' },
25+
{ capabilities: { tools: {} } },
26+
);
27+
28+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
29+
tools: [
30+
{
31+
name: 'echo_upper',
32+
description: 'Upper-case the input text (deterministic showcase demo tool).',
33+
inputSchema: {
34+
type: 'object',
35+
properties: { text: { type: 'string', description: 'Text to upper-case' } },
36+
required: ['text'],
37+
},
38+
},
39+
],
40+
}));
41+
42+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
43+
if (req.params.name !== 'echo_upper') {
44+
return { content: [{ type: 'text', text: `unknown tool: ${req.params.name}` }], isError: true };
45+
}
46+
const text = String(req.params.arguments?.text ?? '');
47+
const upper = text.toUpperCase();
48+
return {
49+
content: [{ type: 'text', text: upper }],
50+
structuredContent: { upper },
51+
};
52+
});
53+
54+
await server.connect(new StdioServerTransport());

examples/app-showcase/src/automation/flows/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,55 @@ export const ShowcaseDeclarativeConnectorPingFlow = defineFlow({
445445
],
446446
});
447447

448+
/**
449+
* MCP Connector Echo (ADR-0097 / #3056) — dispatches through the DECLARATIVE
450+
* MCP instance `showcase_mcp_tools` (src/system/connectors/index.ts), which the
451+
* `mcp` provider materialized at boot from the in-repo stdio fixture server:
452+
* its `tools/list` became the action list, and this `connector_action` invokes
453+
* the `echo_upper` tool via `tools/call`. The run's captured output
454+
* (`structuredContent.upper === 'OBJECTSTACK'`) proves the full chain —
455+
* metadata entry → provider factory → MCP handshake → flow dispatch — with no
456+
* external dependency. Completes the `provider: 'mcp'` acceptance demo from
457+
* ADR-0097 §6, deferred at #3017.
458+
*/
459+
export const ShowcaseMcpConnectorEchoFlow = defineFlow({
460+
name: 'showcase_mcp_connector_echo',
461+
label: 'MCP Connector Echo (ADR-0097)',
462+
description:
463+
'Dispatches the echo_upper tool of showcase_mcp_tools — a declarative MCP connector instance materialized ' +
464+
'at boot from the in-repo stdio fixture (scripts/mcp-fixture.mjs).',
465+
type: 'autolaunched',
466+
// Surface the tool result on the run output, so the flow run view (and the
467+
// dogfood proof) can assert the MCP round-trip observably.
468+
variables: [{ name: 'echo.structuredContent', type: 'json', isOutput: true }],
469+
nodes: [
470+
{
471+
id: 'start',
472+
type: 'start',
473+
label: 'On Task Created',
474+
config: {
475+
objectName: 'showcase_task',
476+
triggerType: 'record-after-create',
477+
},
478+
},
479+
{
480+
id: 'echo',
481+
type: 'connector_action',
482+
label: 'echo_upper via MCP (declarative)',
483+
connectorConfig: {
484+
connectorId: 'showcase_mcp_tools',
485+
actionId: 'echo_upper',
486+
input: { text: 'objectstack' },
487+
},
488+
},
489+
{ id: 'end', type: 'end', label: 'End' },
490+
],
491+
edges: [
492+
{ id: 'e1', source: 'start', target: 'echo' },
493+
{ id: 'e2', source: 'echo', target: 'end' },
494+
],
495+
});
496+
448497
/**
449498
* Task Follow-up Reminder — the worked `wait` (durable timer) example.
450499
*
@@ -1327,6 +1376,7 @@ export const allFlows = [
13271376
ScheduledDigestFlow,
13281377
TaskCompletedRestPingFlow,
13291378
ShowcaseDeclarativeConnectorPingFlow,
1379+
ShowcaseMcpConnectorEchoFlow,
13301380
TaskFollowUpFlow,
13311381
NotifyOwnerSubflow,
13321382
TaskDoneNotifyOwnerFlow,

examples/app-showcase/src/coverage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export const STACK_COLLECTION_COVERAGE: Record<string, KindCoverage> = {
184184
status: 'demonstrated',
185185
files: ['src/system/connectors/index.ts', 'src/automation/flows/index.ts'],
186186
notes:
187-
'Both connector kinds are demonstrated. (1) Provider-bound INSTANCES (ADR-0097 / #2977): StatusApiConnector declares `provider: rest` (inline config) and StatusOpenApiConnector declares `provider: openapi` with its OpenAPI document referenced as a package-relative FILE PATH (#3016, read at boot and confined to the package root) — both are materialized into live, dispatchable connectors at boot; ShowcaseDeclarativeConnectorPingFlow calls the rest instance via connector_action and both appear in GET /connectors. (2) Catalog DESCRIPTOR (#2612): ErpCatalogConnector has no provider, so it stays inert metadata; enabled:false marks the deliberate catalog entry and silences the boot audit. Plugin-registered connectors (ConnectorRestPlugin/ConnectorSlackPlugin in objectstack.config.ts) are also exercised by the connector flows.',
187+
'Both connector kinds are demonstrated. (1) Provider-bound INSTANCES (ADR-0097 / #2977) cover all three generic executors: StatusApiConnector declares `provider: rest` (inline config), StatusOpenApiConnector declares `provider: openapi` with its OpenAPI document referenced as a package-relative FILE PATH (#3016, read at boot and confined to the package root), and DevToolsMcpConnector declares `provider: mcp` against the in-repo stdio fixture (scripts/mcp-fixture.mjs, #3056) — spawned under the host declarativeStdio allowlist (#3055) and mapped tools/list → actions. All three are materialized into live, dispatchable connectors at boot; ShowcaseDeclarativeConnectorPingFlow calls the rest instance and ShowcaseMcpConnectorEchoFlow calls the mcp instance via connector_action, and all appear in GET /connectors. (2) Catalog DESCRIPTOR (#2612): ErpCatalogConnector has no provider, so it stays inert metadata; enabled:false marks the deliberate catalog entry and silences the boot audit. Plugin-registered connectors (ConnectorRestPlugin/ConnectorSlackPlugin in objectstack.config.ts) are also exercised by the connector flows.',
188188
},
189189
};
190190

examples/app-showcase/src/system/connectors/index.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,45 @@ export const StatusOpenApiConnector = defineConnector({
9191
auth: { type: 'none' },
9292
});
9393

94+
/**
95+
* ADR-0097 provider-bound instance, **mcp** form (#3056, completing the live
96+
* demo deferred from #3017): the entry points at the tiny in-repo MCP fixture
97+
* server (`scripts/mcp-fixture.mjs`) over a **stdio** transport. At boot the
98+
* `mcp` provider factory (ConnectorMcpPlugin in objectstack.config.ts) spawns
99+
* the fixture, calls `tools/list`, and maps its one deterministic tool
100+
* (`echo_upper`) to a connector action — no network, no ports, no boot-ordering
101+
* coupling, so the demo is CI-deterministic.
102+
*
103+
* Two platform behaviors are dogfooded here:
104+
* - **#3055 stdio policy**: a declarative stdio transport spawns a local
105+
* process from metadata, so it is denied by default; the host opts in via
106+
* `new ConnectorMcpPlugin({ declarativeStdio: ['node'] })` in
107+
* objectstack.config.ts. Remove that option and boot fails loudly with the
108+
* opt-in instructions — try it.
109+
* - **#3049 degrade + retry**: if the fixture were unreachable (e.g. the
110+
* script path wrong), boot would NOT crash — the instance registers
111+
* `state: 'degraded'` on GET /connectors and the platform retries with
112+
* backoff until it heals.
113+
*/
114+
export const DevToolsMcpConnector = defineConnector({
115+
name: 'showcase_mcp_tools',
116+
label: 'Dev Tools (Declarative MCP Instance)',
117+
type: 'api',
118+
description:
119+
'Provider-bound declarative connector instance (ADR-0097) backed by a Model Context Protocol server: the ' +
120+
'in-repo stdio fixture (scripts/mcp-fixture.mjs). Its tools/list becomes the action list — echo_upper is ' +
121+
'dispatched end-to-end by ShowcaseMcpConnectorEchoFlow.',
122+
provider: 'mcp',
123+
providerConfig: {
124+
// Spawned at materialization; the path is relative to the app's working
125+
// directory (`os dev`/`serve` run from the app root). The command must be
126+
// allowlisted by the host's declarativeStdio policy (#3055) — see
127+
// objectstack.config.ts.
128+
transport: { kind: 'stdio', command: 'node', args: ['./scripts/mcp-fixture.mjs'] },
129+
},
130+
auth: { type: 'none' },
131+
});
132+
94133
export const ErpCatalogConnector = defineConnector({
95134
name: 'showcase_erp_catalog',
96135
label: 'ERP Integration (Catalog Descriptor)',
@@ -143,4 +182,9 @@ export const ErpCatalogConnector = defineConnector({
143182
enabled: false,
144183
});
145184

146-
export const allConnectors: Connector[] = [StatusApiConnector, StatusOpenApiConnector, ErpCatalogConnector];
185+
export const allConnectors: Connector[] = [
186+
StatusApiConnector,
187+
StatusOpenApiConnector,
188+
DevToolsMcpConnector,
189+
ErpCatalogConnector,
190+
];

packages/create-objectstack/src/templates/blank/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ curl -b cookies.txt "http://localhost:3000/api/v1/data/<your_object>"
2626
- `objectstack.config.ts` — environment manifest (objects, API, plugins)
2727
- `src/objects/` — object definitions (one file per object)
2828

29+
## Connectors (default providers)
30+
31+
The template ships the three generic connector executors — `rest`, `openapi`,
32+
and `mcp` — in `plugins:`. With them installed, a declarative `connectors:`
33+
entry that names a `provider` becomes a live, dispatchable connector at boot
34+
(pure metadata, no plugin code):
35+
36+
```ts
37+
connectors: [{
38+
name: 'billing',
39+
provider: 'openapi',
40+
providerConfig: { spec: './specs/billing-openapi.json', baseUrl: 'https://billing.example.com' },
41+
auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' }, // env var name — never an inline secret
42+
}]
43+
```
44+
45+
Remove executors you don't want from `plugins:`. Note: a declarative `mcp`
46+
instance over a **stdio** transport spawns a local process from metadata and is
47+
denied by default — opt in with
48+
`new ConnectorMcpPlugin({ declarativeStdio: ['<trusted-command>'] })`.
49+
2950
## Verify your changes
3051

3152
After editing any metadata, run:

packages/create-objectstack/src/templates/blank/objectstack.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { defineStack } from '@objectstack/spec';
2+
import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
3+
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
4+
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
25
import * as objects from './src/objects/index.js';
36

47
export default defineStack({
@@ -16,4 +19,19 @@ export default defineStack({
1619
engines: { protocol: '^15' },
1720
},
1821
objects: Object.values(objects),
22+
23+
// The three GENERIC connector executors (rest / openapi / mcp) ship in the
24+
// default preset (#3056, ADR-0097): with them installed, a declarative
25+
// `connectors:` entry that names a `provider` materializes into a live,
26+
// dispatchable connector at boot — integrations stay pure metadata, no
27+
// plugin code required. Zero-arg = each contributes only its provider
28+
// factory (an unused factory costs nothing at runtime). Remove any you
29+
// don't want; brand connectors (Slack, …) are installed separately.
30+
//
31+
// Security note (#3055): a declarative `mcp` instance with a STDIO
32+
// transport spawns a local process from metadata, so it is denied by
33+
// default — opt in per host with
34+
// `new ConnectorMcpPlugin({ declarativeStdio: ['<trusted-command>'] })`.
35+
// http transports need no opt-in.
36+
plugins: [new ConnectorRestPlugin(), new ConnectorOpenApiPlugin(), new ConnectorMcpPlugin()],
1937
});

0 commit comments

Comments
 (0)