# Integrations

> Tenant-level catalog identities for OpenAPI specs, GraphQL endpoints, MCP servers, and plugin-registered sources; detection, registration, and auth method descriptors.

- Repository: RhysSullivan/executor
- GitHub: https://github.com/RhysSullivan/executor
- Human docs: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052
- Complete Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/llms-full.txt

## Source Files

- `apps/docs/concepts/integrations.mdx`
- `packages/core/api/src/integrations/api.ts`
- `packages/core/api/src/handlers/integrations.ts`
- `packages/core/sdk/src/integration.ts`
- `packages/plugins/openapi/src/sdk/plugin.ts`
- `packages/plugins/mcp/src/sdk/plugin.ts`

---

---
title: "Integrations"
description: "Tenant-level catalog identities for OpenAPI specs, GraphQL endpoints, MCP servers, and plugin-registered sources; detection, registration, and auth method descriptors."
---

An **integration** is a tenant-level catalog row keyed by `slug` that names an API surface (OpenAPI spec, GraphQL endpoint, MCP server, or plugin-registered source). Core stores only catalog identity (`slug`, `name`, `description`, owning `kind`) plus an opaque `config` blob the owning plugin writes and reads. Integrations do not carry credentials; owner-scoped [connections](/connections) bind auth templates to credential providers and materialize per-connection tools.

## Integration vs connection

| Concern | Integration | Connection |
| --- | --- | --- |
| Scope | Tenant catalog (shared) | Owner-scoped (`org` or `user`) |
| Identity | `slug` | `(owner, integration, name)` |
| Holds secrets | No | Yes (via credential provider) |
| Produces tools | Declares the surface; tool timing depends on plugin | Creates or refreshes tool rows for that owner |
| Auth selection | Declares `authMethods` (derived from config) | Binds one `template` slug from those methods |

<Info>
The built-in `executor` integration (`kind: "built-in"`, `canRemove: false`) ships in every workspace and exposes core control tools (for example `executor.integrations.list`) without connection setup.
</Info>

## Catalog model

Persisted integrations live in the `integration` table. The HTTP and SDK surfaces project a public `Integration` type with no credentials and no plugin-internal config.

<ResponseField name="slug" type="IntegrationSlug">
Stable catalog identifier within the tenant. Branded string; used as the `<integration>` segment in tool addresses.
</ResponseField>

<ResponseField name="name" type="string">
Display name. Falls back to `description` then `slug` on legacy rows.
</ResponseField>

<ResponseField name="description" type="string">
Agent-visible context: what the API is and when to use it. Editable via `PATCH /integrations/:slug` without touching plugin config.
</ResponseField>

<ResponseField name="kind" type="string">
Owning plugin id (for example `openapi`, `graphql`, `mcp`, `built-in`).
</ResponseField>

<ResponseField name="canRemove" type="boolean">
`false` for static or built-in integrations registered at plugin boot. `true` for user-added catalog entries.
</ResponseField>

<ResponseField name="canRefresh" type="boolean">
`true` when the owning plugin supports `connections.refresh` (re-resolve tools for existing connections).
</ResponseField>

<ResponseField name="authMethods" type="AuthMethodDescriptor[]">
Derived projection of the plugin's `authenticationTemplate`. Always present, possibly empty. Not stored as a DB column.
</ResponseField>

<ResponseField name="displayUrl" type="string (optional)">
Non-secret URL for catalog UI (favicons). Projected by the plugin's `describeIntegrationDisplay` hook.
</ResponseField>

Plugin-owned config (`IntegrationConfig`) is opaque to core: auth templates, spec references, MCP endpoints, static headers, and similar fields. Only the owning plugin decodes it at register, configure, and execute time.

```text
Tenant catalog                         Owner partition
+---------------------------+          +---------------------------+
| integration (slug)        |          | connection                |
|  plugin_id -> kind        |  1..n    |  (owner, integration,     |
|  config (opaque JSON)     | <------> |   name)                   |
|  can_remove, can_refresh  |          |  template -> auth method  |
+---------------------------+          |  provider -> credential     |
                                       +---------------------------+
                                                |
                                                v
                                       tool rows (per connection)
```

## Integration kinds

| `kind` | Registration entrypoint | Tool production timing | Typical `config` contents |
| --- | --- | --- | --- |
| `openapi` | `executor.openapi.addSpec` / `openapi.addSpec` tool | At add (`putOperations`); `updateSpec` rebuilds all connections | `specHash`, `sourceUrl`, `authenticationTemplate`, optional `baseUrl` / headers |
| `graphql` | `executor.graphql.addIntegration` / `graphql.addIntegration` tool | At connection create or refresh (introspection deferred) | `endpoint`, `authenticationTemplate`, optional `introspectionJson` |
| `mcp` | `executor.mcp.addServer` / `mcp.addServer` tool | At connection create or refresh (live discovery) | `transport` (`remote` or `stdio`), `endpoint` or `command`, `authenticationTemplate` |
| `built-in` | Plugin `staticSources` at boot | Static tools under `executor.*` namespace | None (not persisted) |

Multi-API providers (for example Google, Microsoft) bundle several APIs into one integration so one credential covers the whole provider.

## Registration

Plugins register integrations through `ctx.core.integrations.register` inside their extension methods. Core's `register` **upserts** on slug collision (idempotent boot re-registration), but explicit add flows (`addSpec`, `addServer`, `addIntegration`) **reject** duplicate slugs with `IntegrationAlreadyExistsError`.

<Steps>
<Step title="Choose a slug and describe the surface">
Pick a stable `slug` (for example `stripe`, `github_mcp`). Set `name` and `description` for humans and agents. MCP and GraphQL default the slug from the endpoint when omitted.
</Step>

<Step title="Register via the owning plugin">
Use the plugin extension or its static control tools:

<CodeGroup>
```bash title="OpenAPI (HTTP API)"
POST /openapi/specs
# body: spec, slug, optional baseUrl, authenticationTemplate
```

```bash title="MCP (HTTP API)"
POST /mcp/servers
# body: transport, name, endpoint, optional authenticationTemplate
```

```bash title="GraphQL (HTTP API)"
POST /graphql/integrations
# body: endpoint, optional slug, authenticationTemplate
```
</CodeGroup>
</Step>

<Step title="Verify catalog projection">
List or fetch the integration and confirm `kind`, `authMethods`, and `canRefresh`:

```bash
GET /integrations
GET /integrations/:slug
```
</Step>

<Step title="Create a connection">
Registering an integration does not authenticate. Create an owner-scoped connection that picks an `authMethods[].template` and supplies credential values or completes OAuth. See [Configure credentials](/configure-credentials).
</Step>
</Steps>

### Auth template merge

Plugins that carry a slugged `authenticationTemplate` array (OpenAPI, GraphQL, MCP) use `mergeAuthTemplates`: an incoming entry with a matching slug replaces in place; slug-less entries receive a generated `custom_<id>` slug. Configure flows (`openapi.configure`, `mcp.configureAuth`, GraphQL `configureAuth`) support `mode: "merge"` (default) or `mode: "replace"`.

### Integration presets

Each plugin may declare `integrationPresets` (popular integrations in the web UI). Presets are catalog metadata only; registering still goes through the plugin add flow. Available presets are exposed via `ctx.core.integrations.presets()` aggregated across loaded plugins.

## URL detection

`POST /integrations/detect` (SDK: `executor.integrations.detect(url)`) asks every loaded plugin with a `detect` hook whether the URL belongs to it. Results are returned as an array; the UI ranks by `confidence` (`high` > `medium` > `low`).

<ParamField body="url" type="string" required>
URL to probe. Trimmed before dispatch. Max length 2048 characters.
</ParamField>

<ResponseField name="kind" type="string">
Plugin id that recognized the URL (`openapi`, `graphql`, `mcp`, ...).
</ResponseField>

<ResponseField name="confidence" type="high | medium | low">
Match tier. Multiple plugins may claim the same URL; the UI picks the best rank.
</ResponseField>

<ResponseField name="endpoint" type="string">
Normalized endpoint the plugin will use if the user proceeds.
</ResponseField>

<ResponseField name="name" type="string">
Suggested display name (spec title, hostname, etc.).
</ResponseField>

<ResponseField name="slug" type="string">
Suggested catalog slug.
</ResponseField>

| Plugin | High-confidence signal | Low-confidence fallback |
| --- | --- | --- |
| `openapi` | URL resolves to parseable OpenAPI document | None |
| `graphql` | Live introspection succeeds | URL contains `graphql` token |
| `mcp` | Unauthenticated tool discovery or wire-shape probe confirms MCP | URL contains `mcp` token |

<RequestExample>
```bash
curl -X POST "$EXECUTOR_BASE/integrations/detect" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://api.example.com/openapi.json"}'
```
</RequestExample>

<ResponseExample>
```json
[
  {
    "kind": "openapi",
    "confidence": "high",
    "endpoint": "https://api.example.com/openapi.json",
    "name": "Example API",
    "slug": "example_api"
  }
]
```
</ResponseExample>

## Auth method descriptors

`authMethods` is a **derived projection** of each plugin's stored `authenticationTemplate`. Core calls the plugin's `describeAuthMethods(record)` when serving `integrations.list` or `integrations.get`. Malformed config degrades to `[]` rather than failing the catalog read.

| `kind` | Meaning | Typical fields |
| --- | --- | --- |
| `oauth` | OAuth2 authorization code or discovered MCP OAuth | `oauth.authorizationUrl`, `oauth.tokenUrl`, `oauth.scopes`, `oauth.discoveryUrl`, `oauth.supportsDynamicRegistration` |
| `apikey` | API key or bearer rendered via placements | `placements[]` with `carrier` (`header` or `query`), `name`, `prefix`, `variable` |
| `header` | Custom header auth (alias of apikey projection) | Same as `apikey` |
| `none` | No credential required | Empty placements |

<ParamField body="id" type="string">
Stable id within the integration (usually the template slug).
</ParamField>

<ParamField body="template" type="string">
Auth-template slug a connection binds against (`AuthTemplateSlug`).
</ParamField>

<ParamField body="placements" type="PlacementDescriptor[]">
Where credential values render on outbound requests. `variable` names the connection input (`token` when omitted). `literal` renders a static value with no credential input.
</ParamField>

<Note>
OpenAPI derives methods from declared security schemes when `authenticationTemplate` is omitted at add time. MCP OAuth methods carry `discoveryUrl` (the MCP endpoint) and `supportsDynamicRegistration: true`; authorize/token endpoints are discovered at connect time.
</Note>

## HTTP API

Integrations are served under the v2 catalog group (formerly `sources`). Routes carry no scope segment; tenant binding comes from request auth.

| Method | Path | Summary |
| --- | --- | --- |
| `GET` | `/integrations` | List all integrations (persisted + static sources) |
| `GET` | `/integrations/:slug` | Fetch one integration |
| `PATCH` | `/integrations/:slug` | Update `name` and/or `description` only |
| `DELETE` | `/integrations/:slug` | Remove when `canRemove` is true |
| `POST` | `/integrations/detect` | Probe a URL across plugins |

:::endpoint GET /integrations/:slug
Fetch the public integration projection including derived `authMethods`.

**Params:** `slug` (IntegrationSlug)

**Success:** `IntegrationResponse`

**Errors:** `404` IntegrationNotFoundError
:::

:::endpoint PATCH /integrations/:slug
Update user-editable catalog metadata. Does not modify plugin `config`.

<ParamField body="name" type="string">
New display name.
</ParamField>

<ParamField body="description" type="string">
New agent-visible description.
</ParamField>

**Success:** Updated `IntegrationResponse`

**Errors:** `404` IntegrationNotFoundError
:::

:::endpoint DELETE /integrations/:slug
Remove an integration and cascade-delete its connections, tools, and definitions.

**Success:** `{ "removed": true }`

**Errors:** `409` IntegrationRemovalNotAllowedError when `canRemove` is false
:::

## SDK and plugin surfaces

```typescript
// Public catalog read
const all = await executor.integrations.list();
const one = await executor.integrations.get(IntegrationSlug.make("stripe"));

// URL detection
const matches = await executor.integrations.detect("https://mcp.example.com/mcp");

// Plugin-side registration (inside extension)
yield* ctx.core.integrations.register({
  slug: IntegrationSlug.make("my_api"),
  name: "My API",
  description: "Internal billing API",
  config: { /* opaque plugin config */ },
  canRemove: true,
  canRefresh: true,
});
```

Plugin extension methods exposed on `executor`:

| Plugin | Key methods | Static control tools |
| --- | --- | --- |
| OpenAPI | `addSpec`, `updateSpec`, `previewSpec`, `configure` | `executor.openapi.previewSpec`, `executor.openapi.addSpec` |
| GraphQL | `addIntegration`, `configure`, `configureAuth` | `executor.graphql.addIntegration`, `executor.graphql.getIntegration` |
| MCP | `addServer`, `probeEndpoint`, `configureAuth` | `executor.mcp.probeEndpoint`, `executor.mcp.addServer` |

Core also exposes catalog helpers on `ctx.core.integrations` inside plugins: `register`, `update`, `list`, `get`, `remove`, `detect`, `presets`, `configureSchemas`.

## Lifecycle and refresh

- **Remove:** Allowed only when `canRemove: true`. Deletes all connections, tool rows, and definitions for that `slug`. Built-in and boot-registered static integrations return `409`.
- **Refresh:** When `canRefresh: true`, `connections.refresh` re-runs the plugin's `resolveTools` hook. OpenAPI `updateSpec` additionally refreshes every connection after swapping stored operations.
- **Config revision:** Updating integration `config` stamps `config_revised_at`. Connections whose `tools_synced_at` is older lazily rebuild on next read (cross-subject owner policy).

<Warning>
Re-adding an existing slug via explicit add flows fails with `integration_already_exists`. To change auth methods or spec content, use the plugin's update/configure paths (`updateSpec`, `configure`, `configureAuth`) instead of registering again.
</Warning>

## Related pages

<CardGroup>
<Card title="Add integrations" href="/add-integrations">
Step-by-step add flows for OpenAPI, GraphQL, and MCP from the web UI or CLI.
</Card>
<Card title="Connections" href="/connections">
Owner-scoped credentials bound to integration auth templates.
</Card>
<Card title="Configure credentials" href="/configure-credentials">
Credential providers, OAuth minting, and placement-based auth templates.
</Card>
<Card title="Tools" href="/tools">
How integrations and connections produce discoverable, invocable tools.
</Card>
<Card title="HTTP API reference" href="/http-api-reference">
Full route payloads and error shapes for the integrations group.
</Card>
</CardGroup>
