--- name: fruxon-create-integration displayName: Create a New Fruxon Integration description: > Bootstrap a brand-new integration (with API tools or Python code tools) end-to-end via the CLI: schema discovery, body authoring, tools, then a clean handoff to the user for credentials. Load when the user asks to add an integration; the flow first checks whether one already exists and only builds a new one if nothing suitable is found. --- You are now operating as a Fruxon integration-bootstrapping specialist. ## Mental model: shape vs instance Every integration has two layers: - **Shape** — the integration definition + its tools. WHAT auth methods it supports, WHAT parameters the user fills in, WHAT tools it exposes. Authored via the CLI in this flow. - **Instance (config)** — the actual secret values. API keys, OAuth grants, tenant ids. Entered by the user on a Fruxon page: the dashboard, or the page a credential link opens. NEVER author or paste secrets through the CLI. Your job is the shape. The user does the instance. Make the handoff smooth. ## Workflow 1. **Check what already exists FIRST.** Before building anything, confirm the platform doesn't already cover this. Don't rebuild an integration that's there, and don't create a duplicate slug: ``` fruxon integrations list --query fruxon integrations get custom_ # 404 = slug is free ``` In agent mode these default to JSON, so the listing already carries each integration's `description` — enough to judge suitability without a per-item `get`. To see what an existing integration can already DO (before deciding it's missing a tool), enumerate its tools: ``` fruxon tools list # JSON includes each tool's description ``` If a matching integration already exists: - The user just wants to **use** it (wire its tools into an agent) → stop here and switch to the `fruxon-use-integrations` skill instead. - It exists but is **missing a tool** the user needs (confirmed via `tools list`) → don't re-create the integration; grab the tool schema from step 2, then author just the new tool against the existing `integrationId` (skip to step 5). Only proceed with a brand-new integration when nothing suitable exists. 2. **Discover the body shape.** Don't guess fields: ``` fruxon integrations create --schema > integration.schema.json fruxon tools create --schema > tool.schema.json ``` The schemas are the source of truth for required fields, types, and discriminated unions. For the wider CLI surface (every command, its flags, types, curated examples) run `fruxon describe` — designed for an AI-agent driver to consume in one call. 3. **Author `integration.json`.** Key fields: - `id` — `custom_` followed by a snake_case slug (`custom_github`, `custom_acme_crm`). The prefix is required — the server rejects a custom integration id without it — and keeps custom ids from ever colliding with a built-in integration. - `displayName` — what shows in the dashboard. - `configMetadata.authMetadata[]` — describe each auth method the user will supply via the UI: `{ id, displayName, parametersMetadata: [{name, displayName, secret: true}] }`. Mark secret fields with `"secret": true` so the dashboard masks them. Common shapes: - **API key**: one param, secret, displayName "API Key". - **Bearer token**: one param, secret, displayName "Access Token". - **OAuth (user)**: typically zero params here — the dashboard runs the consent flow. - `configMetadata.parametersMetadata[]` — NON-secret connection params (workspace id, region, base URL). Optional. 4. **Create the integration.** ``` fruxon integrations create --file ./integration.json ``` Note the handoff URL printed at the end — you'll reuse it when telling the user what to do. 5. **Author each tool as its own JSON file.** Two flavors. Both flavors share these top-level fields: - `id` — snake_case slug, unique within the integration. - `integrationId` — the CLI auto-fills this from the positional arg on `tools create `; you may omit it. If you do include it, it must match. - `actionType` — one of `READ_ONLY` (default for GET / pure functions), `REVERSIBLE` (write that can be undone), or `IRREVERSIBLE` (sends mail, charges money). Note: the enum is reversibility-flavored, NOT CRUD-flavored — there is no `WRITE`. Defaults to `READ_ONLY` when omitted. - `parametersMetadata[]` — inputs the agent passes when calling the tool. Each has `name`, `displayName`, `description`, and optional `required`, `secret`, `defaultValue`. ## Placeholders (CRITICAL) All templating uses **double curly braces**: `{{name}}` — NOT `{name}`. Single curlies are passed through literally and your tool will silently fail with the placeholder text embedded in the URL/header/body. Names are **flat** — there is no `auth.` / `config.` / `param.` prefix. All sources merge into one dict: - Tool call parameters (whatever the agent passes). - Integration config non-secret parameters (workspace id, region, base URL — declared in `configMetadata.parametersMetadata`). - Built-ins: `{{tenant}}` (the active tenant slug). Auth secrets are **NOT exposed as placeholders**. The auth provider injects the `Authorization` header (or query param) automatically based on the integration's `authMetadata.type`. DO NOT write `Authorization` in your tool's `headers`. Examples: - `BEARER_TOKEN` → adds `Authorization: Bearer `. - `API_KEY` → adds the header configured in `authMetadata.authSettings` (e.g. `X-Api-Key: `). - Custom prefix needed (Discord "Bot", G2 "Token token="): set `authSettings: { Scheme: "Bot", ValuePrefix: "" }` on the `authMetadata` — still no template needed in the tool. **API tool** (HTTP endpoint): ```json { "id": "list_repos", "displayName": "List repositories", "description": "List the authenticated user's repos.", "descriptor": { "apiTool": { "httpMethod": "GET", "url": "https://api.github.com/user/repos", "headers": { "Accept": "application/json" }, "queryParameters": { "visibility": "{{visibility}}" }, "resultType": "JSON" } }, "parametersMetadata": [ {"name": "visibility", "description": "all|public|private"} ] } ``` Notice: no `Authorization` header — auth is automatic. `resultType` is one of `JSON`, `STRING`, `BOOLEAN`, `INTEGER`, `TOOL`. There is no `JSON_ARRAY` / `JSON_OBJECT` — `JSON` covers both. **Code tool** (Python, sandboxed): ```json { "id": "summarize_diff", "integrationId": "github", "displayName": "Summarize diff", "descriptor": { "codeTool": { "code": "result = diff[:200]", "timeoutSeconds": 30, "resultType": "STRING" } }, "parametersMetadata": [{"name": "diff", "required": true}] } ``` Parameters are injected as named Python variables at the top of the script — reference them directly (`diff` above, no `kwargs` dance). Assign to `result` to return data. Optional `packages: ["requests", ...]` installs PyPI deps into the sandbox; `timeoutSeconds` is 1–120 (default 60). 6. **Create each tool.** ``` fruxon tools create --file ./list_repos.json ``` 7. **Hand off to the user. This is the UX-critical step.** Get the user a link that connects it: - **API key, bearer token or username + password** shared by the workspace (an APPLICATION-level method): `fruxon integrations connect -o json` creates the config and returns its `configId` and a `url`. The page it opens takes the secret, tests it and publishes the config. Required connection settings go in as `--param name=value`. - **OAuth** shared by the workspace: the same command, `fruxon integrations connect -o json` (`--scope` for what to ask). Whoever opens the link signs in to the provider, and the account is connected and the config published. - **Per-user credentials** (each person connects their own): nothing to fill now; each user is asked on first use. Compose a SHORT, FRIENDLY message that includes: - One line: what you built (integration + tool count). - The link, and one line on what the user will be asked to paste (or, for OAuth, which account to sign in with). - The fallback: the dashboard (`fruxon integrations open `). Example: ``` ✓ Created the `github` integration with 4 tools. Now connect your account — agents can't call the tools until this is done. Open this link, sign in to Fruxon and paste a GitHub Personal Access Token: → https://app.fruxon.com/credential-entry#t=… Or connect it in the dashboard: fruxon integrations open github ``` DO NOT ask the user for the secret in chat. DO NOT offer to put it in the JSON file. A Fruxon page (the link's, or the dashboard) is the only place secrets are accepted, by design. ## After the user connects Once the user reports the config is set up, verify: ``` fruxon integrations configs list fruxon integrations verify # tests the auth fruxon tools run --tool -c # end-to-end ``` ## Optional: expose the integration as an MCP server If the user wants to call these tools from Claude Desktop, Cursor, or other MCP-capable clients, enable MCP via: ``` fruxon integrations configs list # grab the configId fruxon integrations mcp enable --config ``` The server auto-mints a dedicated `mcp:invoke`-scoped key, bound to this MCP only, and prints the raw secret EXACTLY ONCE. Frame the handoff to the user as: ``` ✓ MCP server enabled for ``. Add to Claude Desktop config (`claude_desktop_config.json`): "fruxon-": { "url": "", "headers": { "Authorization": "Bearer " } } The secret won't be shown again — copy it now. If you lose it, rotate from the dashboard. ``` DO NOT use `fruxon integrations mcp enable` to "look at" the state — that's `mcp status`. Enable is the one-shot mint; save the output the first time. Rotation is dashboard-only on purpose (audit + intentional secret-minting). ## Common pitfalls - Hardcoding the secret in `apiTool.headers` instead of using a `{auth.token}` placeholder. The secret should never be in the integration definition; it lives in the config. - Forgetting `"secret": true` on auth parameters — dashboard won't mask the input field. - Authoring tools before the integration exists (server returns 404 — order matters). - Rebuilding an integration the platform already has. Run the existence check first (step 1) — `fruxon integrations list --query ` — and reuse/extend instead of duplicating. - `id` collisions: both `integration.id` and `tool.id` are unique-per-tenant. If `fruxon integrations get ` returns 404, the id is free. Otherwise pick a fresh slug (keeping the `custom_` prefix). - Skipping the dashboard handoff and instead writing the secret into the JSON. Don't. Even if the user offers it. The shape / instance separation is a security boundary, not a suggestion.