diff --git a/.changeset/connect-agent-ui-bundle.md b/.changeset/connect-agent-ui-bundle.md new file mode 100644 index 0000000000..91218ee2f6 --- /dev/null +++ b/.changeset/connect-agent-ui-bundle.md @@ -0,0 +1,14 @@ +--- +'@objectstack/mcp': minor +'@objectstack/platform-objects': patch +--- + +feat(mcp): plugin-carried "Connect an agent" Setup page (#2714 Phase 1) + +The MCP plugin now registers a Setup page (`connect_agent`) plus its +navigation entry under Integrations — the nav lives and dies with the +capability (cloud ADR-0009 principle) and follows the surface's default-on +switch: an opted-out deployment (`OS_MCP_SERVER_ENABLED=false`) gets no page +and no entry. The page body is the `mcp:connect-agent` SDUI widget provided +by objectui (objectui#2372): env MCP URL, per-client connect cards, SKILL.md +download, API-key minting. zh-CN nav label included. diff --git a/content/docs/ai/agents.mdx b/content/docs/ai/agents.mdx index b8e21a5753..d888487314 100644 --- a/content/docs/ai/agents.mdx +++ b/content/docs/ai/agents.mdx @@ -46,7 +46,9 @@ instead — see the callout in the [AI Overview](/docs/ai).) Every deployment serves MCP at `/api/v1/mcp` by default (a core platform capability — set `OS_MCP_SERVER_ENABLED=false` to opt out), with two -authentication tracks: +authentication tracks: The in-product entry point is the +**Setup → Connect an Agent** page: per-client connect snippets, the portable +SKILL.md download (`GET /api/v1/mcp/skill`), and API-key minting. - **OAuth 2.1 (interactive clients — recommended).** Each deployment is its own spec-compliant authorization server: the endpoint publishes diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx index 139f1c6ac3..1af7dcac0a 100644 --- a/content/docs/getting-started/build-with-claude-code.mdx +++ b/content/docs/getting-started/build-with-claude-code.mdx @@ -319,6 +319,17 @@ claude mcp add --transport http support-desk https://your-deployment.example.com # first tool use opens a browser login — you're connected as yourself ``` +Or install the [official plugin](https://github.com/objectstack-ai/claude-plugin) — +it bundles the portable ObjectStack agent skill and a guided `/objectstack:connect` +command (one plugin serves every deployment; the URL is the only input): + +```bash +claude plugin marketplace add objectstack-ai/claude-plugin +``` + +Admins find every client's copy-paste-ready connect snippet — plus the SKILL.md +download and API-key minting — on the **Setup → Connect an Agent** page. + For headless callers (CI, scripts), mint an API key instead (`POST /api/v1/keys`, shown once) and pass it as a header: diff --git a/packages/mcp/src/__tests__/plugin.test.ts b/packages/mcp/src/__tests__/plugin.test.ts index 121892308e..c38e0b67ff 100644 --- a/packages/mcp/src/__tests__/plugin.test.ts +++ b/packages/mcp/src/__tests__/plugin.test.ts @@ -236,4 +236,37 @@ describe('MCPServerPlugin', () => { await plugin.destroy(); }); }); + + describe('connect-agent UI bundle (plugin-carried Setup page)', () => { + async function startWithManifest(env?: string) { + if (env === undefined) delete process.env.OS_MCP_SERVER_ENABLED; + else process.env.OS_MCP_SERVER_ENABLED = env; + const manifest = { register: vi.fn() }; + const ctx = createMockPluginContext({ manifest }); + const plugin = new MCPServerPlugin(); + await plugin.init(ctx as any); + await plugin.start(ctx as any); + // Replay the kernel:ready hooks the plugin registered. + for (const call of (ctx.hook as any).mock.calls) { + if (call[0] === 'kernel:ready') await call[1](); + } + return manifest; + } + + it('registers the Connect-an-Agent page by default (surface default-on)', async () => { + const manifest = await startWithManifest(); + expect(manifest.register).toHaveBeenCalledTimes(1); + const bundle = manifest.register.mock.calls[0][0]; + expect(bundle.id).toBe('com.objectstack.mcp.connect-agent-ui'); + expect(bundle.pages[0].name).toBe('connect_agent'); + expect(bundle.navigationContributions[0].app).toBe('setup'); + expect(bundle.navigationContributions[0].items[0].pageName).toBe('connect_agent'); + }); + + it('registers nothing when the MCP surface is opted out', async () => { + const manifest = await startWithManifest('false'); + expect(manifest.register).not.toHaveBeenCalled(); + }); + }); + }); diff --git a/packages/mcp/src/connect-ui.ts b/packages/mcp/src/connect-ui.ts new file mode 100644 index 0000000000..b8ce85b0c6 --- /dev/null +++ b/packages/mcp/src/connect-ui.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * "Connect an agent" Setup page — plugin-carried UI metadata (#2714 Phase 1, + * objectui#2363). + * + * The page ships WITH the MCP capability (same principle as the marketplace + * pages in `@objectstack/cloud-connection`, cloud ADR-0009: the nav lives and + * dies with the capability — no MCP plugin, no entry). The page body is the + * SDUI widget `mcp:connect-agent`, provided by objectui's console app-shell: + * it reads `/discovery` for the environment's MCP URL, renders per-client + * connect cards (claude.ai / Claude Desktop / Claude Code / Cursor / VS Code / + * Codex), mints API keys for headless callers, and links the SKILL.md + * download (`GET /api/v1/mcp/skill`). + * + * Registered by {@link MCPServerPlugin} on `kernel:ready`, gated on the same + * default-on switch as the HTTP surface — an opted-out deployment + * (`OS_MCP_SERVER_ENABLED=false`) gets no page and no nav entry. + */ + +export const CONNECT_AGENT_PAGE = { + name: 'connect_agent', + label: 'Connect an Agent', + type: 'app' as const, + template: 'default', + kind: 'full' as const, + isDefault: false, + regions: [ + { + name: 'header', + width: 'full' as const, + components: [ + { + type: 'page:header', + properties: { + title: 'Connect an Agent', + subtitle: + 'Give any MCP-capable AI client governed access to this environment — ' + + 'every call runs under the caller\'s own permissions and row-level security.', + icon: 'bot', + }, + }, + ], + }, + { + name: 'main', + width: 'large' as const, + components: [{ type: 'mcp:connect-agent', properties: {} }], + }, + ], +}; + +export const CONNECT_AGENT_UI_BUNDLE = { + id: 'com.objectstack.mcp.connect-agent-ui', + namespace: 'sys', + version: '0.1.0', + type: 'plugin', + scope: 'system', + name: 'Connect an Agent UI', + description: 'Setup page + navigation for connecting MCP clients to this environment.', + pages: [CONNECT_AGENT_PAGE], + navigationContributions: [ + { + app: 'setup', + group: 'group_integrations', + priority: 110, + items: [ + { + id: 'nav_connect_agent', + type: 'page', + pageName: 'connect_agent', + label: 'Connect an Agent', + icon: 'bot', + }, + ], + }, + ], +}; diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 9042dd2485..68000010c2 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -30,3 +30,4 @@ export { OBJECTSTACK_SKILL_DESCRIPTION, } from './skill.js'; export type { RenderSkillOptions } from './skill.js'; +export { CONNECT_AGENT_PAGE, CONNECT_AGENT_UI_BUNDLE } from './connect-ui.js'; diff --git a/packages/mcp/src/plugin.ts b/packages/mcp/src/plugin.ts index c6e6c1cb01..4658801cd1 100644 --- a/packages/mcp/src/plugin.ts +++ b/packages/mcp/src/plugin.ts @@ -1,11 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { Plugin, PluginContext } from '@objectstack/core'; -import { readEnvWithDeprecation } from '@objectstack/types'; +import { readEnvWithDeprecation, isMcpServerEnabled } from '@objectstack/types'; import type { IAIService, IDataEngine, IMetadataService } from '@objectstack/spec/contracts'; import { MCPServerRuntime } from './mcp-server-runtime.js'; import type { MCPServerRuntimeConfig } from './mcp-server-runtime.js'; import type { ToolRegistry } from './types.js'; +import { CONNECT_AGENT_UI_BUNDLE } from './connect-ui.js'; /** * Configuration options for the MCPServerPlugin. @@ -137,6 +138,19 @@ export class MCPServerPlugin implements Plugin { ); } + // ── Plugin-carried Setup UI (cloud ADR-0009 principle) ── + // "Connect an agent" page + nav entry ship WITH the MCP capability and + // follow the HTTP surface's default-on switch: an opted-out deployment + // advertises nothing, so it gets no page either. + if (isMcpServerEnabled()) { + ctx.hook('kernel:ready', async () => { + try { + const manifest = ctx.getService<{ register(m: unknown): void }>('manifest'); + manifest?.register?.(CONNECT_AGENT_UI_BUNDLE); + } catch { /* no manifest service (bare kernels, tests) */ } + }); + } + // Trigger hook for other plugins to extend MCP await ctx.trigger('mcp:ready', this.runtime); } diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index 90be67d37e..4c04e80758 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -58,6 +58,7 @@ export const zhCN: TranslationData = { nav_sharing_rules: { label: '共享规则' }, nav_record_shares: { label: '记录共享' }, nav_api_keys: { label: 'API 密钥' }, + nav_connect_agent: { label: '连接智能体' }, nav_approval_processes: { label: '审批流程' }, nav_approval_requests: { label: '审批申请' },