Skip to content

feat(skills): external MCP server per skill ('skill calls home' v2) #37

@teslashibe

Description

@teslashibe

Problem

Skills that need compute (ephemeris, image processing, data pipelines, etc.) currently require Go code compiled into the host binary (backend/internal/mcp/platforms/). This means:

  • Skill authors must fork the template to add compute
  • Every skill is coupled to the host's Go version + dependency graph
  • Skills can't be authored in Python, TypeScript, Rust, etc.
  • Adding/removing a skill requires redeploying the whole backend

The current astrology demo (internal/astrology/ + platforms/astrology.go) proves the tool surface works but violates the modularity goal: the code lives inside the template, not in the skill repo.

Goal

Each skill repo can optionally ship its own MCP server. The host discovers it, connects to it, and the per-user agent gains the skill's tools — without touching the host binary or redeploying the backend.

This is the same architecture Claude Desktop (stdio MCP) and Anthropic Managed Agents (URL MCP) already use. We're just making the template aware that skills can bring their own server.

Design

Skill folder gains skill.yaml

name: astrology
description: Birth charts and transit forecasts.
mcp_server:
  transport: http          # "http" or "stdio"
  url: http://astrology:9090/mcp/v1    # for http transport (docker-compose service name)
  command: ["./bin/astrology-mcp"]      # for stdio transport (local dev)
  image: ghcr.io/teslashibe/astrology-skill:latest  # optional: docker image to pull

SKILL.md + reference.md are still uploaded to Anthropic's Skills API (the instructions layer). skill.yaml drives the compute layer.

New table: skill_mcp_servers

CREATE TABLE skill_mcp_servers (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    skill_id    UUID NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
    transport   TEXT NOT NULL DEFAULT 'http',  -- 'http' | 'stdio'
    url         TEXT,
    command     TEXT[],
    image       TEXT,
    healthy     BOOLEAN NOT NULL DEFAULT false,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(skill_id)
);

Upload flow changes

Service.UploadDir / Service.UploadZip:

  1. Upload SKILL.md + reference files to Anthropic (existing flow, unchanged)
  2. If skill.yaml exists and has mcp_server, parse it and upsert into skill_mcp_servers
  3. If transport is http, smoke-test tools/list against the URL; set healthy = true on success
  4. Files listed in skill.yaml that are part of the MCP server (Go binaries, Dockerfiles, etc.) are NOT uploaded to Anthropic — they stay local / containerized

Provisioner changes

internal/agent/provision.go createAgent:

  1. Query skill_mcp_servers WHERE healthy = true
  2. For each, add a BetaManagedAgentsURLMCPServerParams entry to MCPServers (alongside the existing engagement server)
  3. Add a corresponding BetaManagedAgentsMCPToolsetParams entry to Tools
  4. Enforce Anthropic's 10-server cap; log warnings for overflow (same pattern as the 20-skill cap)

Local dev: docker-compose overlay

docker-compose.skills.yml:

services:
  astrology:
    build: ./skills/astrology/mcp
    ports: ["9090:9090"]

Operator runs: docker compose -f docker-compose.yml -f docker-compose.skills.yml up

The skill's MCP server starts as a sidecar; the host API discovers it via skill_mcp_servers.url.

Astrology extraction

Move internal/astrology/ and platforms/astrology.go out of the template into teslashibe/astrology-skill:

teslashibe/astrology-skill/
├── SKILL.md
├── reference.md
├── skill.yaml
├── mcp/
│   ├── main.go          # tiny MCP server using mcptool
│   ├── go.mod
│   └── Dockerfile
└── README.md

The template ships with zero domain-specific MCP tools. All 14 social platforms stay in-process (they share auth, rate limits, and credential storage — different pattern).

User Stories

  • As a skill author, I want to ship my skill as a self-contained repo with its own MCP server (in any language), so that I can add compute capabilities to any fork of the template without modifying the host codebase.
  • As a template forker, I want to make skills-sync a directory containing skill folders with skill.yaml, so that the host automatically discovers and connects to each skill's MCP server alongside the built-in platform tools.
  • As a backend operator, I want skill MCP servers to run as Docker sidecar containers, so that a crashing skill doesn't take down the host API, and I can scale/restart them independently.
  • As a backend operator, I want a health check on each skill MCP server (smoke tools/list), so that a misconfigured skill doesn't silently break agent provisioning.

Acceptance Criteria

skill.yaml parsing

  • Given a skill folder with a valid skill.yaml containing mcp_server.transport: http and mcp_server.url, when make skills-sync runs, then the skill is uploaded to Anthropic AND a row is inserted into skill_mcp_servers with the URL and healthy = true (assuming the server is reachable).
  • Given a skill folder without skill.yaml, when make skills-sync runs, then it uploads to Anthropic only (existing behavior, no skill_mcp_servers row).

Provisioner wiring

  • Given 2 healthy skill MCP servers in the DB, when a new user creates their first session, then the Anthropic Agent has 3 MCPServers (engagement + 2 skill servers) and 3 corresponding MCPToolset entries in Tools.
  • Given 11 healthy skill MCP servers (over the 10-cap), when provisioning, then the oldest 10 are attached and a warning is logged naming the overflow.

Health check

  • Given a skill.yaml pointing at a URL that returns a valid tools/list response, when syncing, then healthy = true.
  • Given a skill.yaml pointing at an unreachable URL, when syncing, then healthy = false, a warning is logged, and the row is still persisted (so a restart can recheck).

Astrology extraction

  • Given the teslashibe/astrology-skill repo running as a Docker sidecar, when an agent calls astrology_birth_summary, then it routes through the skill's MCP server (not in-process code) and returns the correct chart JSON.

Negative paths

  • Given a skill.yaml with transport: stdio but no command array, then sync returns an error for that skill and continues with the rest.
  • Given a skill MCP server crashes mid-session, then the agent receives a tool_error for that tool; all other tools (including other skills and the built-in engagement server) continue working.

Out of scope (V2+)

  • Automatic Docker image pulling + lifecycle management (operator deploys skill containers themselves for now)
  • Skill marketplace / registry browser
  • stdio transport for Managed Agents (Anthropic only supports URL MCP servers for Managed Agents today; stdio is for local Claude Code / Desktop)
  • Per-team skill isolation
  • Skill-to-skill communication

Implementation plan

Step What Files
1 Migration 00006_skill_mcp_servers.sql 1 file
2 Parse skill.yaml in internal/skills/skills.go extend existing
3 Health-check helper (tools/list smoke test) internal/skills/health.go
4 skill_mcp_servers CRUD in internal/skills/ extend existing
5 Provisioner reads skill_mcp_servers + builds dynamic MCPServers list internal/agent/provision.go
6 Extract astrology to teslashibe/astrology-skill with tiny MCP server new repo
7 docker-compose.skills.yml overlay 1 file
8 Drop internal/astrology/ + platforms/astrology.go from template delete 2 files, update All()
9 Update docs/SKILLS.md with the external MCP pattern extend existing

Constraints

  • Anthropic Managed Agents support URL MCP servers only (no stdio). Skills that need stdio transport are limited to Claude Code / Desktop.
  • 10 MCP servers per agent (Anthropic cap). 1 is always engagement; 9 available for skills.
  • Skill MCP servers must be reachable from Anthropic's cloud (same constraint as the host's engagement server — no localhost in production).
  • For local dev, skill servers run on host.docker.internal or as named Docker Compose services.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions