Skip to main content

kind: Integration

What it is

kind: Integration declares one connected MCP server — either a remote streamable-http endpoint (e.g. Linear’s hosted MCP) or a locally spawned stdio process (e.g. npx -y @some/mcp-server). It is the standalone authoring surface for the case where you want one integration declared once and shared across many crews (scope: workspace), or want to declare a crew-scoped integration outside the bulkier kind: Crew document. The legacy inline mcp_servers: block nested under a crew (see Crew and Workspace) still works and remains the most ergonomic shape for bundling integrations with a crew definition. kind: Integration is the inverse: declare once, scope explicitly. The kind is implemented in internal/manifest/kinds/integration.go.

Scope picks the table

A single scope: discriminator chooses where the row lands:
scopeTableWho sees it
workspace (default)workspace_mcp_serversevery crew in the workspace
crewcrew_mcp_serversonly agents of the named crew
crew scope requires crew_slug; workspace scope rejects it (a crew_slug under workspace scope is treated as an authoring mistake and fails loudly).

slug == name

metadata.slug MUST equal metadata.name — the server keys MCP-server uniqueness on name within the (workspace, crew) tuple, but every cross-kind reference uses slug, so the manifest forces the two to match (the same convention Label uses).

env vs env_mapping

Two env-related maps both land in the same env_json column:
  • env — plain static environment variables (e.g. NODE_ENV: production). The value is the literal string the MCP process sees.
  • env_mapping — the credential-indirection layer. Keys are the env-var the MCP server expects; values are the workspace credential’s name (conventionally identical, but can differ — e.g. {GITHUB_PERSONAL_ACCESS_TOKEN: GH_TOKEN}). At agent run time the resolver looks up each credential by name and substitutes the value before the MCP process starts.
On key collision, env wins (a literal value beats a credential reference for the same key).

YAML schema

apiVersion: crewship/v1        # required — always crewship/v1 for now
kind: Integration              # required — the literal string "Integration"
metadata:
  name: linear                 # required — MCP server name
  slug: linear                 # required — MUST equal metadata.name
spec:
  scope: workspace             # optional — workspace (default) | crew
  crew_slug: code-review       # required iff scope == crew; rejected otherwise
  display_name: Linear         # optional — defaults to metadata.name
  transport: streamable-http   # required — streamable-http | stdio
  endpoint: https://mcp.linear.app/sse   # required for streamable-http
  command: npx                 # required for stdio
  args: [-y, "@some/mcp-server"]         # stdio-only positional args
  env:                         # optional — literal env vars (win on collision)
    NODE_ENV: production
  env_mapping:                 # optional — credential indirection (key → cred name)
    LINEAR_API_KEY: LINEAR_API_KEY
  icon: linear                 # optional — lucide-react slug
  enabled: true                # optional — runtime connect toggle (default true)

Field reference

FieldTypeRequiredNotes
metadata.namestringyesMCP server name; uniqueness key within (workspace, crew).
metadata.slugstringyesMUST equal metadata.name.
spec.scopeenumnoworkspace (default) | crew.
spec.crew_slugstringcond.Required when scope: crew; must be empty when scope: workspace.
spec.display_namestringnoUI label. Defaults to metadata.name server-side (and the manifest mirrors that default so the round-trip diff is stable).
spec.transportenumyesstreamable-http | stdio. The server has no sensible default for “what kind of MCP server is this”.
spec.endpointstringcond.Required for streamable-http; ignored otherwise.
spec.commandstringcond.Required for stdio; ignored otherwise.
spec.args[]stringnostdio positional args. Marshaled into args_json. No empty entries.
spec.envmap[string]stringnoLiteral env vars. No empty keys.
spec.env_mappingmap[string]stringnoCredential references (env-var → credential name). No empty keys or values.
spec.iconstringnolucide-react slug.
spec.enabledboolnoRuntime connect toggle. Defaults to true; pointer-typed so an absent field is distinguishable from false.

Examples

Remote (streamable-http), workspace scope

apiVersion: crewship/v1
kind: Integration
metadata:
  name: linear
  slug: linear
spec:
  transport: streamable-http
  endpoint: https://mcp.linear.app/sse
  env_mapping:
    LINEAR_API_KEY: LINEAR_API_KEY
  icon: linear

Local (stdio), crew scope

apiVersion: crewship/v1
kind: Integration
metadata:
  name: github
  slug: github
spec:
  scope: crew
  crew_slug: code-review
  display_name: GitHub
  transport: stdio
  command: npx
  args: [-y, "@modelcontextprotocol/server-github"]
  env_mapping:
    GITHUB_PERSONAL_ACCESS_TOKEN: GH_TOKEN

Static env plus a credential reference

apiVersion: crewship/v1
kind: Integration
metadata:
  name: custom-tool
  slug: custom-tool
spec:
  transport: stdio
  command: node
  args: [./mcp/server.js]
  env:
    NODE_ENV: production       # literal — wins on key collision
  env_mapping:
    API_TOKEN: SERVICE_TOKEN   # resolved from the SERVICE_TOKEN credential
  enabled: true

CLI reference

There is no dedicated crewship integration per-kind admin command — integrations are authored through the manifest pipeline (or installed as part of a Connector / Recipe). The relevant CLI surface is the global apply/export flow:
CommandDescription
crewship apply --file integration.yamlDeclarative create/update (workspace or crew scope).
crewship apply --dir ./manifests/Walk a directory; crews resolve before crew-scoped integrations.
crewship apply --file integration.yaml --dry-runPlan only — surfaces a dangling crew_slug and shows scope-change replaces.
crewship export workspaceRound-trip — emits one document per integration across both scopes.

REST endpoint mapping

Manifest fieldPOST/PATCH body fieldNotes
metadata.namename
spec.display_namedisplay_nameDefaults to name.
spec.transporttransport
spec.endpointendpointstreamable-http only.
spec.commandcommandstdio only.
spec.argsargs_jsonJSON-encoded string on the wire.
spec.env + spec.env_mappingenv_jsonMerged map, JSON-encoded string.
spec.iconicon
spec.enabledenabled
Endpoints used:
VerbWorkspace scopeCrew scope
POST/api/v1/integrations/api/v1/crews/{crewId}/integrations
GET/api/v1/integrations/api/v1/crews/{crewId}/integrations
PATCH/api/v1/integrations/{id}/api/v1/crews/{crewId}/integrations/{id}
DELETE/api/v1/integrations/{id}/api/v1/crews/{crewId}/integrations/{id}

Validation rules

IntegrationDocument.Validate enforces:
  • apiVersion / kind, when set, equal crewship/v1 / Integration.
  • metadata.name and metadata.slug are non-empty, and slug == name.
  • transport is required and one of streamable-http | stdio.
  • streamable-http requires a non-empty endpoint; stdio requires a non-empty command.
  • scope, when set, is workspace | crew.
  • crew_slug is required iff scope: crew and rejected under scope: workspace.
  • env / env_mapping have no empty keys; env_mapping has no empty values; args has no empty entries.
  • When WorkspaceContext carries crew data, a crew-scoped crew_slug must reference a declared or remote crew.

Apply behavior

ApplyUpsert (default)

  • No remote on this scope → ActionCreate: POST to the workspace or crew endpoint.
  • Remote on the matching scope, fields drift → ActionUpdate: a sparse PATCH. args_json / env_json are compared after JSON normalisation so key-reordering doesn’t produce phantom drift.
  • Remote matches exactly → ActionUnchanged.

Scope change (workspace ↔ crew)

The two scopes are different tables and there is no “move” endpoint, so a changed scope emits a Delete + Create pair, both visible in the dry-run with a “scope change” note. The delete cascades any agent bindings on the old row — review the plan before re-running with --yes.

Round-trip via export

crewship export workspace calls ExportIntegrations, which walks both the workspace scope and every crew’s crew scope, decoding args_json back into spec.args and env_json back into spec.env. Env lossiness: the server has no column distinguishing literal env from credential-reference env_mapping (both live in env_json), so every entry comes back under spec.env on export. If you need the env_mapping shape preserved, keep your source YAML as the source of truth and re-export to a side file rather than overwriting the original. Output is sorted by scope, then crew, then slug for stable diffs.

See also

  • Crew — can declare integrations inline via mcp_servers:.
  • Connector — install-only OAuth connectors (Linear, GitHub, …).
  • Recipe — catalog installs that may bundle integrations.
  • Backend: internal/api/workspace_integrations.go, internal/api/crew_integrations.go.
  • This kind’s Go implementation: internal/manifest/kinds/integration.go.