diff --git a/apps/app/pr/published-workflows-mcp.curl.txt b/apps/app/pr/published-workflows-mcp.curl.txt new file mode 100644 index 000000000..0ad2b913b --- /dev/null +++ b/apps/app/pr/published-workflows-mcp.curl.txt @@ -0,0 +1,134 @@ +# Real-OpenCode curl verification — published-workflows MCP +# +# Captured against a live OpenWork server with managed OpenCode +# (OPENWORK_MANAGE_OPENCODE=1) bound to a workspace at +# ~/openwork-mcp-test containing a single skill: echo. +# +# Server: bun apps/server/src/cli.ts --host 127.0.0.1 --port 4747 +# --token $CLIENT_TOKEN --host-token $HOST_TOKEN +# --workspace ~/openwork-mcp-test --verbose +# +# Outcome: A (success). tools/call returned the agent's text reply +# with no isError, confirming the bridge runs through real OpenCode +# /session and /session/:id/message. + +$ export HOST_TOKEN=owt_test_host +$ export CLIENT_TOKEN=owt_test_client +$ BASE=http://127.0.0.1:4747 + +# --- Step 3: list workspaces, capture id --- +$ curl -s "$BASE/workspaces" -H "Authorization: Bearer $CLIENT_TOKEN" | jq +{ + "items": [ + { + "id": "ws_30eed34021d9", + "name": "openwork-mcp-test", + "path": "/Users/harshithpeta/openwork-mcp-test", + "preset": "starter", + "workspaceType": "local", + "baseUrl": "http://127.0.0.1:56999", + "directory": "/Users/harshithpeta/openwork-mcp-test", + "opencode": { + "baseUrl": "http://127.0.0.1:56999", + "directory": "/Users/harshithpeta/openwork-mcp-test", + "username": "", + "password": "" + } + } + ], + "activeId": "ws_30eed34021d9" +} +$ WS_ID=ws_30eed34021d9 + +# --- Step 4: confirm skill name --- +$ curl -s "$BASE/workspace/$WS_ID/skills" -H "Authorization: Bearer $CLIENT_TOKEN" \ + | jq '.items[].name' +"echo" + +# --- Step 5: publish the skill --- +$ curl -s -X POST "$BASE/workspace/$WS_ID/published-workflows" \ + -H "x-openwork-host-token: $HOST_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "skillName": "echo", + "description": "Echo the input back verbatim", + "inputSchema": { + "type": "object", + "properties": { "input": { "type": "string", "description": "Text to echo" } }, + "required": ["input"] + } + }' | jq +{ + "id": "c3b84969-a9d1-45c8-ad4f-ae6b33a502a9", + "workspaceId": "ws_30eed34021d9", + "skillName": "echo", + "toolName": "echo", + "description": "Echo the input back verbatim", + "createdAt": 1777580681405, + "inputSchema": { + "type": "object", + "properties": { "input": { "type": "string", "description": "Text to echo" } }, + "required": ["input"] + }, + "token": "pwt_233a57851f024a09a86cadaff77ae2bf536e1a50b937446b9a43c8ff50c184b0" +} +$ TOKEN=pwt_233a57851f024a09a86cadaff77ae2bf536e1a50b937446b9a43c8ff50c184b0 + +# --- Step 7: initialize --- +$ curl -s -X POST "$BASE/published/$TOKEN/mcp" \ + -H "content-type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | jq +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { "tools": {} }, + "serverInfo": { + "name": "openwork-published-workflow", + "version": "0.1.0" + } + } +} + +# --- Step 8: tools/list --- +$ curl -s -X POST "$BASE/published/$TOKEN/mcp" \ + -H "content-type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | jq +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "echo", + "description": "Echo the input back verbatim", + "inputSchema": { + "type": "object", + "properties": { "input": { "type": "string", "description": "Text to echo" } }, + "required": ["input"] + } + } + ] + } +} + +# --- Step 9: tools/call (real bridge through OpenCode) --- +$ curl -s -X POST "$BASE/published/$TOKEN/mcp" \ + -H "content-type: application/json" \ + -d '{ + "jsonrpc":"2.0","id":3,"method":"tools/call", + "params":{"name":"echo","arguments":{"input":"hello from MCP"}} + }' | jq +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "```json\n{\n \"input\": \"hello from MCP\"\n}\n```" + } + ] + } +} diff --git a/apps/app/pr/published-workflows-mcp.md b/apps/app/pr/published-workflows-mcp.md new file mode 100644 index 000000000..3f2136593 --- /dev/null +++ b/apps/app/pr/published-workflows-mcp.md @@ -0,0 +1,251 @@ +# Publish OpenWork workflows as MCP-callable tools + +Closes: N/A — Path B submission / exploratory product PR. + +## Commits + +Reviewable in three logical passes: + +1. `feat(server): publish workflows as MCP tools` — storage, + admin routes, MCP transport, sync OpenCode bridge, server tests. +2. `feat(app): publish workflows UI in skills settings` — Publish action, + modal, list panel, store + HTTP helpers, English i18n keys. +3. `chore(i18n): mirror publishedWorkflows keys to all locales` — + English placeholders across `ca / es / fr / ja / pt-BR / th / vi / zh`. + +## Why + +OpenWork already runs OpenCode agents against a workspace, with skills, +plugins, and MCPs wired in. What it cannot do today is invert that +relationship: there is no way for an _external_ MCP client (Claude +Desktop, Cursor, the OpenCode CLI on a teammate's machine, an automation +in n8n) to call back into a workspace and trigger one of those agents. + +`ARCHITECTURE.md` (line 152) literally describes MCP as the right primitive +for "authenticated third-party flows… when 'auth + capability surface' is +the product boundary," and the same doc lists "frictionless publishing +without signup" as a Skill-Registry roadmap goal. This PR delivers the +narrowest useful version of both. + +The product framing is **publishing a workflow**, not "publishing a skill": + +- Skills are markdown context that guides an agent _inside_ a session. +- A workflow is the executable thing: a session running a specific agent, + primed with a specific skill, against a specific workspace. + +So the user-visible verb is _Publish workflow_. The artifact is a tool an +external MCP client can list and call. The transport is a single URL with +a token in the path; the same token IS the authorization. Stateless POST, +single HTTP turn, real result back. + +## Storage + +Same on-disk pattern as `TokenService` and the env file from +`environment-variables.md`. JSON file, sha256-hashed tokens, never store +the plaintext. + +| OS | Path | +| --- | --- | +| Linux / macOS | `~/.config/openwork/published-workflows.json` | +| Windows | `%APPDATA%\openwork\published-workflows.json` | + +Override via `OPENWORK_PUBLISHED_WORKFLOWS_STORE` (mirrors +`OPENWORK_TOKEN_STORE`). File shape: + +```json +{ + "schemaVersion": 1, + "updatedAt": 1714000000000, + "workflows": [ + { + "id": "uuid", + "tokenHash": "sha256-hex", + "workspaceId": "ws_a", + "skillName": "summarize", + "toolName": "summarize", + "description": "Summarize input text", + "agent": "build", + "inputSchema": { "type": "object", "properties": { "input": { "type": "string" } } }, + "createdAt": 1714000000000 + } + ] +} +``` + +`PublishedWorkflowsService` (apps/server/src/published-workflows.ts) +exposes `list / get / create / revoke / findByToken`. The plaintext token +is returned **only** from `create()`; from then on the only way to use it +is via the MCP transport route, which hashes the inbound URL token and +matches by hash. + +## Server + +Three host-token admin routes, scoped per workspace: + +- `GET /workspace/:id/published-workflows` → `{ items: [...] }` +- `POST /workspace/:id/published-workflows` → `{ id, token, ... }` (201). + Body: `{ skillName, description, toolName?, agent?, inputSchema?, label? }`. +- `DELETE /workspace/:id/published-workflows/:workflowId` → `{ ok: true }`. + +Each create/revoke is mirrored into the workspace audit log. + +## MCP transport + +One public route, registered for `POST/GET/DELETE`: + +```text +/published/:token/mcp +``` + +Auth is the URL-embedded token. Unknown token → JSON-RPC `-32001` +(HTTP 401). Workspace removed from config after publish → JSON-RPC +`-32002` (HTTP 410). Anything else falls through to the JSON-RPC +dispatcher in `apps/server/src/published-mcp.ts`, which implements the +minimal MCP Streamable-HTTP subset: + +- `initialize` → returns `protocolVersion: "2024-11-05"`, `serverInfo`, + `capabilities.tools`. +- `notifications/initialized`, `notifications/cancelled` → 202. +- `ping` → `{}`. +- `tools/list` → returns the single tool descriptor for this token. +- `tools/call` → executes the workflow synchronously and returns + `{ content: [{ type: "text", text }] }`. + +GET (SSE upgrade) and DELETE (session terminate) return `405` and `204` +respectively — the transport is stateless and does not need streams. + +We deliberately do not pull in `@modelcontextprotocol/sdk` or `@hono/mcp`. +`apps/server` is a hand-written Bun fetch router; adding the SDK would +mean either bridging Hono into that router or rewriting it. The handler +is ~150 lines and covers everything Claude Desktop / Cursor / Codex +actually call against a tool server. + +## Synchronous bridge + +`tools/call` runs `executePublishedWorkflow`: + +1. `POST /session` against the workspace's OpenCode → new session id. +2. Build a prompt: ``Run the `` skill with the following input: ```json …`````. +3. `POST /session/:id/prompt` (the **synchronous** OpenCode endpoint — + the SDK's `client.session.prompt`, not `prompt_async`) with optional + `agent` from the publication record. +4. `Promise.race` against a 60s timer (`OPENWORK_PUBLISHED_WORKFLOW_TIMEOUT_MS` + override) — a stuck agent cannot pin the worker. +5. Pull text parts out of the assistant response and return them. + +Errors inside `execute` are caught by the JSON-RPC dispatcher and +returned as `{ isError: true, content: [{ type: "text", text: }] }` +so the calling LLM gets a useful tool error instead of an HTTP 500. + +## UI + +Skills view at `apps/app/src/react-app/domains/settings/pages/skills-view.tsx` +gains a **Publish** action on every installed skill card, plus a new +**Published workflows** panel listing active publications. + +- Publish modal: tool name (defaults to skill name), description, optional + JSON input schema. Validates locally before POST. +- Success state surfaces the full MCP URL with the token **once** (the + token is never returned again — same pattern as `TokenService`). +- Listed rows show the token-redacted URL pattern, copy-pattern, and + revoke. Revoke confirms inline. +- Auto-refreshes on mount; manual refresh button on the panel. +- Gated on a connected OpenWork server — disconnected state shows a + warning toast instead of a 401. +- 25 new i18n keys under `settings.publishedWorkflows.*` + `common.copy` + in `en.ts`, mirrored as English placeholders to all 8 other locales + in a separate `chore(i18n)` commit (matches the + `settings.environment.*` precedent). + +State lives in `apps/app/src/react-app/domains/settings/state/extensions-store.ts`; +HTTP helpers (`listPublishedWorkflows`, `createPublishedWorkflow`, +`revokePublishedWorkflow`) live in `apps/app/src/app/lib/openwork-server.ts` +and use the existing host-token plumbing. + +## How to try it + +The fastest reproducible path is curl-only against a local server. A full +transcript is in +[`published-workflows-mcp.curl.txt`](./published-workflows-mcp.curl.txt); +the short version: + +```bash +# 1. Make a workspace dir with one skill in it +mkdir -p ~/openwork-mcp-test/.opencode/skill/echo +cat > ~/openwork-mcp-test/.opencode/skill/echo/SKILL.md <<'EOF' +--- +description: Echo the input back verbatim +--- +Echo the input back verbatim as a JSON code block. +EOF + +# 2. Run the server with a workspace and explicit tokens +export CLIENT_TOKEN=owt_test_client +export HOST_TOKEN=owt_test_host +pnpm --filter openwork-server dev -- \ + --host 127.0.0.1 --port 4747 \ + --token "$CLIENT_TOKEN" --host-token "$HOST_TOKEN" \ + --workspace ~/openwork-mcp-test --verbose + +# 3. In another terminal: list workspaces, publish the skill, hit MCP +BASE=http://127.0.0.1:4747 +WS_ID=$(curl -s "$BASE/workspaces" -H "Authorization: Bearer $CLIENT_TOKEN" | jq -r '.items[0].id') + +curl -s -X POST "$BASE/workspace/$WS_ID/published-workflows" \ + -H "x-openwork-host-token: $HOST_TOKEN" \ + -H "content-type: application/json" \ + -d '{"skillName":"echo","description":"Echo the input back verbatim"}' | jq +# → { id, token: "pwt_…", … } — copy the token + +TOKEN=pwt_… +curl -s -X POST "$BASE/published/$TOKEN/mcp" \ + -H "content-type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq +``` + +For the UI flow, run the web app against the same server: + +```bash +VITE_OPENWORK_URL=http://127.0.0.1:4747 \ +VITE_OPENWORK_TOKEN="$CLIENT_TOKEN" \ +VITE_OPENWORK_HOST_TOKEN="$HOST_TOKEN" \ +pnpm --filter @openwork/app dev +``` + +Then **Settings → Skills → Publish** on the echo card → modal → submit → +copy the URL from the success notice → confirm the new row appears in +**Published workflows** → drop the URL into Claude Desktop / Cursor or +hit it with curl as above → **Revoke** to invalidate the URL. + +## Tests + +| Layer | File | What | +| --- | --- | --- | +| Server unit | `apps/server/src/published-workflows.test.ts` | 7 tests — empty start, create returns issued token + hides hash, list filters by workspace, findByToken hashes input, revoke invalidates, persistence across instances | +| Server HTTP e2e | `apps/server/src/published-mcp.e2e.test.ts` | 9 tests — admin create + list, missing skillName 400, unknown token 401, MCP `initialize` / `tools/list` / `tools/call` / unknown-tool `-32602`, DELETE 204, revoke breaks the token. Uses an in-process fake OpenCode `Bun.serve` | + +## Verification + +```text +pnpm --filter openwork-server typecheck # clean +pnpm --filter @openwork/app typecheck # clean +bun test # 132 pass, 0 fail +``` + +Also confirmed against a running local server: + +- Server admin routes (`list / create / delete`) and MCP routes + (`initialize / tools/list / tools/call`) succeed against a live + workspace with a managed OpenCode (transcript: + `published-workflows-mcp.curl.txt`). +- Manual UI walkthrough (publish → copy URL → list → revoke) executed + end-to-end against a `pnpm --filter @openwork/app dev` instance + pointed at the local server. + +## Non-goals (follow-ups) + +- Multi-tool publications (one workflow = one tool today). +- OAuth / DCR — bearer-style URL token is enough for MVP. +- Streaming partial results back over SSE — sync `/prompt` is the MVP. +- Public marketplace surface — out of scope; this is private per token. +- Per-call billing / metering — Den-team territory. diff --git a/apps/app/src/app/lib/openwork-server.ts b/apps/app/src/app/lib/openwork-server.ts index 7b7eb1963..25fa88ce4 100644 --- a/apps/app/src/app/lib/openwork-server.ts +++ b/apps/app/src/app/lib/openwork-server.ts @@ -117,9 +117,9 @@ export type OpenworkSessionSnapshot = { messages: OpenworkSessionMessage[]; todos: Todo[]; status: - | { type: "idle" } - | { type: "busy" } - | { type: "retry"; attempt: number; message: string; next: number }; + | { type: "idle" } + | { type: "busy" } + | { type: "retry"; attempt: number; message: string; next: number }; }; export type OpenworkPluginItem = { @@ -680,7 +680,7 @@ async function requestMultipartRaw( baseUrl: string, path: string, options: { method?: string; token?: string; hostToken?: string; body?: FormData; timeoutMs?: number } = {}, -): Promise<{ ok: boolean; status: number; text: string }>{ +): Promise<{ ok: boolean; status: number; text: string }> { const url = `${baseUrl}${path}`; const fetchImpl = resolveFetch(url); const response = await fetchWithTimeout( @@ -701,7 +701,7 @@ async function requestBinary( baseUrl: string, path: string, options: { method?: string; token?: string; hostToken?: string; timeoutMs?: number } = {}, -): Promise<{ data: ArrayBuffer; contentType: string | null; filename: string | null }>{ +): Promise<{ data: ArrayBuffer; contentType: string | null; filename: string | null }> { const url = `${baseUrl}${path}`; const fetchImpl = resolveFetch(url); const response = await fetchWithTimeout( @@ -1227,7 +1227,70 @@ export function createOpenworkServerClient(options: { baseUrl: string; token?: s method: "DELETE", timeoutMs: timeouts.config, }), + + // Published workflows (host-auth admin surface, see + // apps/server/src/published-workflows.ts and + // apps/app/pr/published-workflows-mcp.md). The plaintext token is only + // returned from create(); callers must surface it immediately. + listPublishedWorkflows: (workspaceId: string) => + requestJson<{ items: PublishedWorkflow[] }>( + baseUrl, + `/workspace/${encodeURIComponent(workspaceId)}/published-workflows`, + { token, hostToken, timeoutMs: timeouts.config }, + ), + + createPublishedWorkflow: ( + workspaceId: string, + payload: PublishedWorkflowCreatePayload, + ) => + requestJson( + baseUrl, + `/workspace/${encodeURIComponent(workspaceId)}/published-workflows`, + { + token, + hostToken, + method: "POST", + body: payload, + timeoutMs: timeouts.config, + }, + ), + + revokePublishedWorkflow: (workspaceId: string, workflowId: string) => + requestJson<{ ok: true }>( + baseUrl, + `/workspace/${encodeURIComponent(workspaceId)}/published-workflows/${encodeURIComponent(workflowId)}`, + { token, hostToken, method: "DELETE", timeoutMs: timeouts.config }, + ), }; } +export type PublishedWorkflow = { + id: string; + workspaceId: string; + skillName: string; + toolName: string; + description: string; + agent?: string | null; + label?: string | null; + inputSchema?: Record | null; + createdAt: number; +}; + +export type PublishedWorkflowCreated = PublishedWorkflow & { + token: string; +}; + +export type PublishedWorkflowCreatePayload = { + skillName: string; + description: string; + toolName?: string; + agent?: string; + label?: string; + inputSchema?: Record; +}; + +export function buildPublishedWorkflowMcpUrl(baseUrl: string, token: string): string { + return `${baseUrl.replace(/\/+$/, "")}/published/${encodeURIComponent(token)}/mcp`; +} + export type OpenworkServerClient = ReturnType; diff --git a/apps/app/src/i18n/locales/ca.ts b/apps/app/src/i18n/locales/ca.ts index 12b9d68e3..656ce432b 100644 --- a/apps/app/src/i18n/locales/ca.ts +++ b/apps/app/src/i18n/locales/ca.ts @@ -1436,6 +1436,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "Reinicia OpenCode o l'orquestrador després de canviar aquesta configuració.", "settings.export": "Exporta", "settings.export_failed": "No s'ha pogut exportar l'informe del runtime.", diff --git a/apps/app/src/i18n/locales/en.ts b/apps/app/src/i18n/locales/en.ts index 166391e77..251659786 100644 --- a/apps/app/src/i18n/locales/en.ts +++ b/apps/app/src/i18n/locales/en.ts @@ -92,6 +92,7 @@ export default { "common.choose": "Choose", "common.back": "Back", "common.close": "Close", + "common.copy": "Copy", "common.default_parens": "(default)", "common.done": "Done", "common.edit": "Edit", @@ -1715,6 +1716,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.tab_description_environment": "Save API keys and tokens for local agents, skills, and MCP servers. Secrets stay on this device.", "settings.tab_description_messaging": "Configure router identities and inbox behavior from workspace settings.", "settings.tab_description_model": "Tune the default model, runtime behavior, and assistant output settings.", diff --git a/apps/app/src/i18n/locales/es.ts b/apps/app/src/i18n/locales/es.ts index da84f9ec7..2f0afccbf 100644 --- a/apps/app/src/i18n/locales/es.ts +++ b/apps/app/src/i18n/locales/es.ts @@ -1436,6 +1436,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "Reinicia OpenCode o el orquestador después de cambiar esta configuración.", "settings.export": "Exportar", "settings.export_failed": "No se ha podido exportar el informe del runtime.", diff --git a/apps/app/src/i18n/locales/fr.ts b/apps/app/src/i18n/locales/fr.ts index f79a39fc0..c3f2f1d9d 100644 --- a/apps/app/src/i18n/locales/fr.ts +++ b/apps/app/src/i18n/locales/fr.ts @@ -1436,6 +1436,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "Redémarrez OpenCode ou l'orchestrateur après avoir modifié ce paramètre.", "settings.export": "Exporter", "settings.export_failed": "Échec de l'export du rapport d'exécution.", diff --git a/apps/app/src/i18n/locales/ja.ts b/apps/app/src/i18n/locales/ja.ts index f22e9cdd7..701eef466 100644 --- a/apps/app/src/i18n/locales/ja.ts +++ b/apps/app/src/i18n/locales/ja.ts @@ -1418,6 +1418,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ / OPENCODE_ の名前は OpenWork/OpenCode が管理します。", "settings.environment.validation_shape": "英字・数字・アンダースコアを使用してください。数字で始められません。", "settings.environment.value_label": "値", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "この設定を変更した後、OpenCodeまたはオーケストレーターを再起動してください。", "settings.export": "エクスポート", "settings.export_failed": "ランタイムレポートのエクスポートに失敗しました。", diff --git a/apps/app/src/i18n/locales/pt-BR.ts b/apps/app/src/i18n/locales/pt-BR.ts index 96e94bddf..8dd8edc27 100644 --- a/apps/app/src/i18n/locales/pt-BR.ts +++ b/apps/app/src/i18n/locales/pt-BR.ts @@ -1419,6 +1419,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "Reinicie o OpenCode ou o orchestrator após alterar esta configuração.", "settings.export": "Exportar", "settings.export_failed": "Falha ao exportar relatório de runtime.", diff --git a/apps/app/src/i18n/locales/th.ts b/apps/app/src/i18n/locales/th.ts index 94ac119a1..2287357b5 100644 --- a/apps/app/src/i18n/locales/th.ts +++ b/apps/app/src/i18n/locales/th.ts @@ -1419,6 +1419,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "รีสตาร์ท OpenCode หรือ orchestrator หลังเปลี่ยนการตั้งค่านี้", "settings.export": "ส่งออก", "settings.export_failed": "ส่งออกรายงานรันไทม์ไม่สำเร็จ", diff --git a/apps/app/src/i18n/locales/vi.ts b/apps/app/src/i18n/locales/vi.ts index 1076b9ebc..31645b47c 100644 --- a/apps/app/src/i18n/locales/vi.ts +++ b/apps/app/src/i18n/locales/vi.ts @@ -1419,6 +1419,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ and OPENCODE_ names are managed by OpenWork/OpenCode.", "settings.environment.validation_shape": "Use letters, digits, and underscores; do not start with a digit.", "settings.environment.value_label": "Value", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "Khởi động lại OpenCode hoặc orchestrator sau khi thay đổi cài đặt này.", "settings.export": "Xuất", "settings.export_failed": "Xuất báo cáo runtime thất bại.", diff --git a/apps/app/src/i18n/locales/zh.ts b/apps/app/src/i18n/locales/zh.ts index 3418fb564..8d1f99114 100644 --- a/apps/app/src/i18n/locales/zh.ts +++ b/apps/app/src/i18n/locales/zh.ts @@ -1422,6 +1422,30 @@ export default { "settings.environment.validation_reserved": "OPENWORK_ 和 OPENCODE_ 名称由 OpenWork/OpenCode 管理。", "settings.environment.validation_shape": "请使用字母、数字和下划线,且不能以数字开头。", "settings.environment.value_label": "值", + "settings.publishedWorkflows.confirm_revoke": "Revoke published workflow {name}? Existing MCP clients using its URL will stop working.", + "settings.publishedWorkflows.copy_url_pattern": "Copy URL pattern", + "settings.publishedWorkflows.empty": "No workflows published yet. Use Publish on an installed skill to expose it as an MCP tool.", + "settings.publishedWorkflows.error_disconnected": "Connect to an OpenWork server to publish workflows.", + "settings.publishedWorkflows.field_description": "Tool description", + "settings.publishedWorkflows.field_description_hint": "Shown to MCP clients when they list this tool.", + "settings.publishedWorkflows.field_tool_name": "Tool name", + "settings.publishedWorkflows.field_tool_name_hint": "How the tool appears to MCP clients. Defaults to the skill name.", + "settings.publishedWorkflows.modal_subtitle": "Expose this skill as an MCP tool callable over HTTP.", + "settings.publishedWorkflows.modal_title": "Publish as MCP tool", + "settings.publishedWorkflows.publish_button": "Publish", + "settings.publishedWorkflows.publish_submit": "Publish workflow", + "settings.publishedWorkflows.publishing": "Publishing…", + "settings.publishedWorkflows.revoke": "Revoke", + "settings.publishedWorkflows.revoked_toast": "Workflow revoked.", + "settings.publishedWorkflows.section_subtitle": "Skills you have exposed as MCP tools. Each has a private URL containing a token; share carefully.", + "settings.publishedWorkflows.section_title": "Published workflows", + "settings.publishedWorkflows.skill_tag": "skill: {name}", + "settings.publishedWorkflows.success_notice": "Workflow published. Copy the URL below — the token is only shown once.", + "settings.publishedWorkflows.token_hint": "The full URL with token was shown when the workflow was published.", + "settings.publishedWorkflows.token_warning": "This URL contains a secret token. Anyone with the URL can call the tool.", + "settings.publishedWorkflows.url_copied": "URL copied to clipboard.", + "settings.publishedWorkflows.url_label": "MCP URL", + "settings.publishedWorkflows.validation_description_required": "Description is required.", "settings.exa_restart_hint": "更改此设置后,请重启OpenCode或编排器。", "settings.export": "导出", "settings.export_failed": "导出运行时报告失败。", diff --git a/apps/app/src/react-app/domains/settings/pages/skills-view.tsx b/apps/app/src/react-app/domains/settings/pages/skills-view.tsx index 830cf4bc6..942b37531 100644 --- a/apps/app/src/react-app/domains/settings/pages/skills-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/skills-view.tsx @@ -12,8 +12,10 @@ import { Copy, Edit2, FolderOpen, + Globe, Loader2, Package, + Plug, Plus, RefreshCw, Rocket, @@ -46,6 +48,12 @@ import type { HubSkillRepo, SkillCard, } from "../../../../app/types"; +import { + buildPublishedWorkflowMcpUrl, + type PublishedWorkflow, + type PublishedWorkflowCreated, + type PublishedWorkflowCreatePayload, +} from "../../../../app/lib/openwork-server"; import { inputClass, modalHeaderButtonClass, @@ -125,6 +133,12 @@ export type SkillsExtensionsStore = { description?: string; }) => void | Promise; uninstallSkill: (name: string) => void | Promise; + publishedWorkflows: () => PublishedWorkflow[]; + publishedWorkflowsStatus: () => string | null; + publishedWorkflowsServerBaseUrl: () => string | null; + refreshPublishedWorkflows: (force?: boolean) => void | Promise; + publishWorkflow: (payload: PublishedWorkflowCreatePayload) => Promise; + revokePublishedWorkflow: (workflowId: string) => Promise; }; export type SkillsViewProps = { @@ -176,6 +190,14 @@ export function SkillsView(props: SkillsViewProps) { const [installingCloudSkillId, setInstallingCloudSkillId] = useState(null); const [denUiTick, setDenUiTick] = useState(0); + const [publishTarget, setPublishTarget] = useState(null); + const [publishToolName, setPublishToolName] = useState(""); + const [publishDescription, setPublishDescription] = useState(""); + const [publishBusy, setPublishBusy] = useState(false); + const [publishError, setPublishError] = useState(null); + const [publishedRecord, setPublishedRecord] = useState(null); + const [revokingWorkflowId, setRevokingWorkflowId] = useState(null); + const showToast = useCallback( (title: string, tone: ToastTone = "info") => { props.onToast?.({ title, tone }); @@ -192,6 +214,7 @@ export function SkillsView(props: SkillsViewProps) { useEffect(() => { void extensions.ensureHubSkillsFresh(); void extensions.ensureCloudOrgSkillsFresh(); + void extensions.refreshPublishedWorkflows(); const onDenSession = () => { setDenUiTick((value) => value + 1); setCloudSessionNonce((value) => value + 1); @@ -201,6 +224,18 @@ export function SkillsView(props: SkillsViewProps) { return () => window.removeEventListener("openwork-den-session-updated", onDenSession); }, [extensions]); + useEffect(() => { + if (!publishTarget) return; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + if (publishBusy) return; + event.preventDefault(); + setPublishTarget(null); + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [publishBusy, publishTarget]); + useEffect(() => { if (!shareTarget) return; const onKeyDown = (event: KeyboardEvent) => { @@ -283,6 +318,10 @@ export function SkillsView(props: SkillsViewProps) { const hubSkills = extensions.hubSkills(); const cloudOrgSkills = extensions.cloudOrgSkills(); const importedCloudSkills = extensions.importedCloudSkills(); + const publishedWorkflows = extensions.publishedWorkflows(); + const publishedWorkflowsStatus = extensions.publishedWorkflowsStatus(); + const publishedServerBaseUrl = extensions.publishedWorkflowsServerBaseUrl(); + const canPublishWorkflows = Boolean(publishedServerBaseUrl); const hubRepo = extensions.hubRepo(); const hubRepos = extensions.hubRepos(); const skillsStatus = extensions.skillsStatus(); @@ -621,6 +660,89 @@ export function SkillsView(props: SkillsViewProps) { } }, [shareUrl, showToast]); + const openPublishModal = useCallback( + (skill: SkillCard) => { + if (props.busy) return; + if (!canPublishWorkflows) { + showToast(t("settings.publishedWorkflows.error_disconnected"), "warning"); + return; + } + setPublishTarget(skill); + setPublishToolName(skill.name); + setPublishDescription(skill.description ?? ""); + setPublishBusy(false); + setPublishError(null); + setPublishedRecord(null); + }, + [canPublishWorkflows, props.busy, showToast], + ); + + const closePublishModal = useCallback(() => { + if (publishBusy) return; + setPublishTarget(null); + setPublishToolName(""); + setPublishDescription(""); + setPublishError(null); + setPublishedRecord(null); + }, [publishBusy]); + + const submitPublish = useCallback(async () => { + if (!publishTarget || publishBusy) return; + const description = publishDescription.trim(); + if (!description) { + setPublishError(t("settings.publishedWorkflows.validation_description_required")); + return; + } + setPublishBusy(true); + setPublishError(null); + try { + const created = await extensions.publishWorkflow({ + skillName: publishTarget.name, + description, + toolName: publishToolName.trim() || undefined, + }); + setPublishedRecord(created); + } catch (error) { + setPublishError(maskError(error)); + } finally { + setPublishBusy(false); + } + }, [extensions, maskError, publishBusy, publishDescription, publishTarget, publishToolName]); + + const copyPublishedUrl = useCallback( + async (url: string) => { + try { + await navigator.clipboard.writeText(url); + showToast(t("settings.publishedWorkflows.url_copied"), "success"); + } catch { + showToast(t("skills.copy_link_failed"), "error"); + } + }, + [showToast], + ); + + const handleRevokeWorkflow = useCallback( + async (workflow: PublishedWorkflow) => { + if (revokingWorkflowId) return; + const confirmed = window.confirm( + t("settings.publishedWorkflows.confirm_revoke", undefined, { + name: workflow.toolName, + }), + ); + if (!confirmed) return; + setRevokingWorkflowId(workflow.id); + try { + await extensions.revokePublishedWorkflow(workflow.id); + showToast(t("settings.publishedWorkflows.revoked_toast"), "success"); + } catch (error) { + showToast(maskError(error), "error"); + } finally { + setRevokingWorkflowId(null); + } + }, + [extensions, maskError, revokingWorkflowId, showToast], + ); + const openSkill = useCallback( async (skill: SkillCard) => { if (props.busy) return; @@ -850,6 +972,24 @@ export function SkillsView(props: SkillsViewProps) {
{t("skills.installed_status")}
+
) : null} + + {publishTarget ? ( + void submitPublish()} + onCopyUrl={copyPublishedUrl} + /> + ) : null} ); } +type PublishedWorkflowsSectionProps = { + workflows: PublishedWorkflow[]; + status: string | null; + serverBaseUrl: string | null; + canPublish: boolean; + revokingWorkflowId: string | null; + onCopyUrl: (url: string) => void | Promise; + onRevoke: (workflow: PublishedWorkflow) => void | Promise; + onRefresh: () => void; + busy: boolean; +}; + +function PublishedWorkflowsSection(props: PublishedWorkflowsSectionProps) { + return ( +
+
+
+

{t("settings.publishedWorkflows.section_title")}

+

+ {t("settings.publishedWorkflows.section_subtitle")} +

+
+ +
+ + {!props.canPublish ? ( +
+ {t("settings.publishedWorkflows.error_disconnected")} +
+ ) : props.status ? ( +
+ {props.status} +
+ ) : props.workflows.length === 0 ? ( +
+ {t("settings.publishedWorkflows.empty")} +
+ ) : ( +
+
+ {props.workflows.map((workflow) => { + const urlPattern = props.serverBaseUrl + ? `${props.serverBaseUrl.replace(/\/+$/, "")}/published//mcp` + : ""; + const isRevoking = props.revokingWorkflowId === workflow.id; + return ( +
+
+
+ +
+
+
+

{workflow.toolName}

+ + {t("settings.publishedWorkflows.skill_tag", undefined, { name: workflow.skillName })} + +
+ {workflow.description ? ( +

+ {workflow.description} +

+ ) : null} + {urlPattern ? ( +

{urlPattern}

+ ) : null} +

+ {t("settings.publishedWorkflows.token_hint")} +

+
+
+
+ {urlPattern ? ( + + ) : null} + +
+
+ ); + })} +
+
+ )} +
+ ); +} + +type PublishWorkflowModalProps = { + skill: SkillCard; + serverBaseUrl: string | null; + toolName: string; + description: string; + onChangeToolName: (value: string) => void; + onChangeDescription: (value: string) => void; + busy: boolean; + error: string | null; + published: PublishedWorkflowCreated | null; + onClose: () => void; + onSubmit: () => void; + onCopyUrl: (url: string) => void | Promise; +}; + +function PublishWorkflowModal(props: PublishWorkflowModalProps) { + const fullUrl = + props.published && props.serverBaseUrl + ? buildPublishedWorkflowMcpUrl(props.serverBaseUrl, props.published.token) + : null; + return ( +
+
+
+
+
+

{t("settings.publishedWorkflows.modal_title")}

+ {props.skill.name} +
+

{t("settings.publishedWorkflows.modal_subtitle")}

+
+ +
+ +
+ {!props.published ? ( +
+ + +