DEV Community

Avaneesh Yadav
Avaneesh Yadav

Posted on Originally published at buildingai.in

Building AI Skills: GitHub Copilot Extensions, Claude Tools, and Reusable Agent Capabilities

Every major AI assistant now supports custom skills — capabilities you build that extend what the AI can do. GitHub Copilot calls them Extensions. Claude calls them Tools (and MCP servers). LangChain calls them Tools. Spring AI calls them Functions. The terminology is a mess, but the concept is identical: give an AI a callable capability with a defined interface, and it will use that capability when the task requires it.

This guide builds one concrete skill — a deployment status checker — across three different platforms: GitHub Copilot Extension, Claude MCP Tool, and a reusable agent skill for any LLM. By the end you'll understand the architecture of each, where they differ, and how to choose the right platform for the job.

What "Skills" Actually Are

Before touching code, it helps to understand the mental model. An AI skill has three parts:

flowchart LR
    subgraph "Skill Anatomy"
        A["<b>Schema</b>\nWhat the skill does,\nwhat inputs it accepts,\nwhat it returns"]
        B["<b>Implementation</b>\nThe actual code that\nexecutes when called"]
        C["<b>Registration</b>\nHow the AI model\ndiscovers the skill\nexists"]
    end

    D[LLM] -->|reads| A
    D -->|"decides to invoke\n(with structured args)"| B
    B -->|result| D
    C -->|exposes A + B to| D

    style A fill:#1e3a5f,color:#7dd3fc
    style B fill:#065f46,color:#6ee7b7
    style C fill:#374151,color:#d1d5db

The LLM reads the schema at inference time, decides whether the skill is relevant to the user's request, calls it with structured arguments extracted from natural language, gets the result, and incorporates it into the response. The human never directly triggers the skill — the LLM does, when it judges the skill will help.

Platform Schema format Registration Invocation
GitHub Copilot Extension Markdown description + REST endpoint GitHub App manifest @your-extension in Copilot Chat
Claude Tools (API) JSON Schema tools:[] parameter on each API call Model invokes via tool_use content block
MCP Server JSON Schema over MCP protocol claude_desktop_config.json or MCP client config Any MCP-compatible client
Spring AI @Tool Auto-derived from method signature ChatClient.tools() call Model invokes through Spring AI framework

The Skill We're Building

Deployment Status Checker — given a service name and optional environment, returns the current deployment status, last deploy time, version, and any active incidents.

This is representative because:

  • It requires an external API call (your deployment platform — Kubernetes, Argo CD, whatever you use)
  • It takes structured parameters
  • It returns structured data the AI formats for humans
  • Every developer wants it in their IDE assistant

We'll implement it three ways and compare what changes.

Platform 1: GitHub Copilot Extension

GitHub Copilot Extensions let you create a custom @your-extension participant in Copilot Chat. Users invoke it with @deploy-status what's the state of the payment service in prod? and your extension receives the conversation, calls your backend, and streams a response back.

Architecture

flowchart TD
    A[Developer types\n@deploy-status in\nCopilot Chat] --> B[GitHub routes message\nto your extension's\nwebhook URL]
    B --> C[Your Extension Server\nNode.js / Java / Python]
    C -->|Verify GitHub\nHMAC signature| D{Valid?}
    D -->|No| E[401 Unauthorized]
    D -->|Yes| F[Parse conversation\nfrom payload]
    F --> G[Call your\ndeployment API]
    G --> H[Optionally call\nCopilot LLM API\nto format response]
    H -->|SSE stream| I[Response appears\nin Copilot Chat]

    style C fill:#1e3a5f,color:#7dd3fc
    style I fill:#065f46,color:#6ee7b7

Setting Up the GitHub App

Create a GitHub App at github.com/settings/apps/new with:

  • Webhook URL: your extension server's /api/github/copilot endpoint
  • Permissions: Copilot Chat → Read (under Account permissions)
  • Copilot: check "Copilot Extension" in the app settings
  • Callback URL: for OAuth if your skill needs to act on behalf of the user
// copilot-extension-manifest.json  defines the extension's capabilities
{
  "name": "deploy-status",
  "description": "Check deployment status, versions, and incidents for any service",
  "instructions": "Use this extension when the user asks about deployment status, service versions, recent deployments, or production incidents. It can check any service across dev, staging, and prod environments.",
  "skill_sets": [
    {
      "name": "deployment",
      "skills": [
        {
          "id": "get-deploy-status",
          "description": "Returns the current deployment status and version for a named service",
          "parameters": {
            "type": "object",
            "properties": {
              "service": { "type": "string", "description": "The service name, e.g. payment-service" },
              "environment": { "type": "string", "enum": ["dev", "staging", "prod"], "default": "prod" }
            },
            "required": ["service"]
          }
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The Extension Server

// server.mjs — Express webhook handler for the Copilot extension
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
import Anthropic from "@anthropic-ai/sdk";

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
const COPILOT_TOKEN  = process.env.COPILOT_TOKEN;  // GitHub-issued token per request

// Verify GitHub's HMAC signature on every webhook
function verifySignature(req) {
  const sig     = req.headers["x-github-token"] ?? "";
  const payload = JSON.stringify(req.body);
  const digest  = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
  return timingSafeEqual(Buffer.from(`sha256=${digest}`), Buffer.from(sig));
}

app.post("/api/github/copilot", async (req, res) => {
  if (!verifySignature(req)) return res.status(401).send("Unauthorized");

  const { messages } = req.body;
  const lastMessage  = messages.at(-1)?.content ?? "";

  // Extract service name from the user's message (basic extraction — Claude does this more elegantly)
  const serviceMatch = lastMessage.match(/\b([a-z][a-z0-9-]+(?:-service|-api|-worker|-gateway))\b/i);
  const service      = serviceMatch?.[1] ?? null;
  const envMatch     = lastMessage.match(/\b(dev|staging|prod|production)\b/i);
  const environment  = envMatch?.[1]?.replace("production", "prod") ?? "prod";

  // Call your actual deployment platform API
  const status = service
    ? await fetchDeployStatus(service, environment)
    : null;

  // Use GitHub's Copilot LLM API to format the response naturally
  const copilotClient = new Anthropic({
    baseURL: "https://api.githubcopilot.com",
    apiKey:  req.headers["x-github-token"],   // use the request token, not your own key
  });

  res.setHeader("Content-Type", "text/event-stream");

  const stream = await copilotClient.messages.stream({
    model:      "gpt-4o",   // GitHub Copilot uses its own model routing
    max_tokens: 1024,
    messages: [
      {
        role:    "system",
        content: "You are a helpful deployment assistant. Format deployment status clearly and concisely. Highlight any issues or incidents prominently."
      },
      ...messages,
      ...(status ? [{
        role:    "system",
        content: `Deployment data for ${service} in ${environment}: ${JSON.stringify(status)}`
      }] : [])
    ]
  });

  // Stream the response back in SSE format
  for await (const chunk of stream) {
    if (chunk.type === "content_block_delta") {
      res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: chunk.delta.text } }] })}\n\n`);
    }
  }
  res.write("data: [DONE]\n\n");
  res.end();
});

async function fetchDeployStatus(service, env) {
  // Replace with your actual deployment API — Argo CD, Kubernetes, Spinnaker, etc.
  const res = await fetch(`${process.env.DEPLOY_API_URL}/services/${service}/status?env=${env}`, {
    headers: { Authorization: `Bearer ${process.env.DEPLOY_API_TOKEN}` }
  });
  if (!res.ok) return { error: `Service ${service} not found in ${env}` };
  return res.json();
}

app.listen(3000, () => console.log("Copilot extension running on :3000"));
Enter fullscreen mode Exit fullscreen mode

[!NOTE]
GitHub Copilot Extensions receive a short-lived x-github-token in each request. Use this token to call GitHub's Copilot LLM API — you don't need your own Anthropic or OpenAI key for the language generation part. You do need your own key if you want to call a different model for specific tasks inside your extension.

Platform 2: Claude Tools (API + Spring AI)

Claude's tool use lets you define callable functions in the API request. The model reads your schema, decides when to call your tool, and sends a structured tool_use request. Your code executes the tool and returns the result. Claude then incorporates the result into its response.

Spring AI Implementation

Spring AI makes Claude tool registration almost declarative with @Tool:

// DeploymentSkill.java — Spring AI tool definition
@Component
public class DeploymentSkill {

    private final DeploymentApiClient deploymentApi;

    @Tool(description = """
        Returns the current deployment status, version, last deploy time, and active incidents
        for a named service. Use when the user asks about service health, deployment state,
        running version, or recent deployments.
        """)
    public DeploymentStatus getDeploymentStatus(
            @ToolParam(description = "The service name, e.g. payment-service or user-api") String service,
            @ToolParam(description = "Environment: dev, staging, or prod. Default is prod.") String environment) {

        return deploymentApi.getStatus(service, environment.isBlank() ? "prod" : environment);
    }

    @Tool(description = """
        Lists the last N deployments for a service, including who triggered them and whether
        they succeeded. Use when the user asks about deployment history or recent changes.
        """)
    public List<DeploymentEvent> getDeploymentHistory(
            @ToolParam(description = "The service name") String service,
            @ToolParam(description = "Number of recent deployments to return, max 20") int limit) {

        return deploymentApi.getHistory(service, Math.min(limit, 20));
    }
}

// Record for structured return — Spring AI serializes this to JSON for Claude
public record DeploymentStatus(
    String  service,
    String  environment,
    String  version,
    String  status,         // "healthy" | "degraded" | "down" | "deploying"
    Instant lastDeployedAt,
    String  deployedBy,
    int     activeIncidents,
    String  incidentSummary // null if no active incidents
) {}
Enter fullscreen mode Exit fullscreen mode
// ChatController.java — wire the tool into every chat session
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatClient chatClient;
    private final DeploymentSkill deploymentSkill;

    @PostMapping
    public Flux<String> chat(@RequestBody ChatRequest req) {
        return chatClient
                .prompt()
                .system("""
                    You are an AI assistant for engineering teams. You have access to deployment
                    status tools. Use them proactively when the user asks about services, 
                    deployments, incidents, or production health.
                    """)
                .user(req.message())
                .tools(deploymentSkill)   // Register the skill — Spring AI handles schema generation
                .stream()
                .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

Spring AI reads your @Tool methods at startup, generates the JSON Schema from the parameter types and @ToolParam descriptions, and includes them in every Claude API call automatically. When Claude decides to use a tool, Spring AI intercepts the tool_use block, invokes your method with the deserialized arguments, and feeds the result back to Claude.

What the API Request Actually Looks Like

Understanding the wire format helps debug issues:

// What Spring AI sends to the Claude API
{
  "model": "claude-opus-5",
  "messages": [{ "role": "user", "content": "What version is payment-service running in prod?" }],
  "tools": [
    {
      "name": "getDeploymentStatus",
      "description": "Returns the current deployment status, version, last deploy time...",
      "input_schema": {
        "type": "object",
        "properties": {
          "service":     { "type": "string", "description": "The service name, e.g. payment-service" },
          "environment": { "type": "string", "description": "Environment: dev, staging, or prod..." }
        },
        "required": ["service"]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
// Claude's response when it decides to invoke the tool
{
  "role": "assistant",
  "content": [
    { "type": "text", "text": "Let me check the deployment status for payment-service in prod." },
    {
      "type": "tool_use",
      "id": "toolu_01XyzAbc",
      "name": "getDeploymentStatus",
      "input": { "service": "payment-service", "environment": "prod" }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
// Your code returns this, Spring AI sends it back to Claude
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01XyzAbc",
      "content": "{\"service\":\"payment-service\",\"environment\":\"prod\",\"version\":\"2.14.1\",...}"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Claude then reads the tool result and writes a natural language response.

Platform 3: MCP Server

The Model Context Protocol (MCP) is an open standard that separates tool implementation from tool consumption. You build an MCP server once, and any MCP-compatible client — Claude Desktop, Claude Code, Cursor, Zed, your own agent — can connect to it without any changes.

flowchart TD
    subgraph "MCP Clients"
        A[Claude Desktop]
        B[Claude Code CLI]
        C[Cursor IDE]
        D[Your Custom Agent]
    end

    subgraph "Your MCP Server"
        E[Tool: get_deploy_status]
        F[Tool: list_deployments]
        G[Tool: trigger_rollback]
        H[Resource: /incidents/active]
    end

    subgraph "Your Backend"
        I[Argo CD API]
        J[PagerDuty API]
        K[GitHub Actions API]
    end

    A <-->|MCP protocol\nover stdio / SSE| E
    B <-->|MCP protocol| E
    C <-->|MCP protocol| F
    D <-->|MCP protocol| G

    E --> I
    F --> I
    G --> K
    H --> J

    style E fill:#1e3a5f,color:#7dd3fc
    style F fill:#1e3a5f,color:#7dd3fc
    style G fill:#1e3a5f,color:#7dd3fc

Spring AI MCP Server

// DeploymentMcpServer.java — Spring AI MCP server implementation
@SpringBootApplication
@EnableMcpServer  // Activate Spring AI's MCP server support
public class DeploymentMcpServer {
    public static void main(String[] args) {
        SpringApplication.run(DeploymentMcpServer.class, args);
    }
}

@McpServer
@Component
public class DeploymentTools {

    private final DeploymentApiClient api;

    @McpTool(
        name        = "get_deploy_status",
        description = "Returns current deployment status, version, and incident summary for a service"
    )
    public DeploymentStatus getStatus(
            @McpParam("service")     String service,
            @McpParam("environment") @Nullable String environment) {
        return api.getStatus(service, environment != null ? environment : "prod");
    }

    @McpTool(
        name        = "list_recent_deployments",
        description = "Lists the last N deployments with timestamps, deployers, and outcomes"
    )
    public List<DeploymentEvent> listDeployments(
            @McpParam("service") String service,
            @McpParam("limit")   @Nullable Integer limit) {
        return api.getHistory(service, limit != null ? Math.min(limit, 20) : 5);
    }

    @McpTool(
        name        = "trigger_rollback",
        description = "Triggers a rollback to the previous stable version for a service in a given environment. REQUIRES explicit user confirmation in the conversation before calling."
    )
    public RollbackResult triggerRollback(
            @McpParam("service")     String service,
            @McpParam("environment") String environment) {
        return api.rollback(service, environment);
    }

    // MCP Resources expose data without requiring the model to "ask" for it
    @McpResource(uri = "deploy://incidents/active", description = "Active production incidents")
    public String activeIncidents() {
        return api.getActiveIncidents().stream()
                .map(i -> "- [%s] %s (since %s)".formatted(i.severity(), i.title(), i.startedAt()))
                .collect(Collectors.joining("\n"));
    }
}
Enter fullscreen mode Exit fullscreen mode
# application.yml — MCP server transport config
spring:
  ai:
    mcp:
      server:
        transport: stdio          # stdio for local tools; sse for remote/multi-user
        name: deploy-status-mcp
        version: 1.0.0
Enter fullscreen mode Exit fullscreen mode

Registering with Claude Code

// ~/.claude/claude_desktop_config.json  or your MCP client's config
{
  "mcpServers": {
    "deploy-status": {
      "command": "java",
      "args": ["-jar", "/opt/tools/deploy-status-mcp.jar"],
      "env": {
        "DEPLOY_API_URL":   "https://deploy.yourcompany.internal",
        "DEPLOY_API_TOKEN": "your-token-here"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

After adding this, Claude Code picks up your tools automatically. Users can run /mcp to list available tools and see get_deploy_status, list_recent_deployments, and trigger_rollback from your server.

Platform 4: Claude Code Skills (SKILL.md)

Claude Code has its own skill system for defining reusable slash commands — procedures Claude follows when you type /deploy-check in a session.

<!-- .claude/skills/deploy-check.md -->
name: deploy-check
description: Check deployment status for a service. Usage: /deploy-check [service] [env]

Check the deployment status for the given service and environment.

## Steps

1. If service name is not provided, ask the user: "Which service? (e.g. payment-service, user-api)"
2. If environment is not provided, default to `prod`
3. Run: `curl -s -H "Authorization: Bearer $DEPLOY_API_TOKEN" \
   "$DEPLOY_API_URL/services/{service}/status?env={env}" | jq .`
4. Format the output:
   - Show service name, environment, version, and status on the first line
   - If status is not "healthy", highlight it prominently with what's wrong
   - Show the last deploy time and who triggered it
   - If there are active incidents, list them with severity and duration
5. If the API call fails (non-200 response), say "Could not reach the deployment API —
   check that DEPLOY_API_TOKEN and DEPLOY_API_URL are set in your environment"

## Output format

Enter fullscreen mode Exit fullscreen mode

[service] @ [env] — v[version] — [status]
Last deployed: [time] by [deployer]
[If incidents:] ⚠️ [N] active incident(s): [summary]

Enter fullscreen mode Exit fullscreen mode

Skills defined in .claude/skills/ are automatically available as /skill-name in every Claude Code session in that project. The CLAUDE.md in your project can also reference them:

<!-- CLAUDE.md -->
## Available slash commands

- `/deploy-check [service] [env]` — Check deployment status and active incidents
- `/review` — Run AI code review on current diff  
- `/cost-check` — Review token usage and estimated costs for this sprint
Enter fullscreen mode Exit fullscreen mode

Writing Skill Descriptions That Work

The single highest-leverage thing you can do to make a skill perform well is write a great description. The model reads your description to decide when to invoke the skill. Bad descriptions lead to missed invocations or wrong invocations.

Bad description:

"Gets deployment info"

Good description:

"Returns the current deployment status, running version, last deploy timestamp, and active PagerDuty incidents for a named service in a specified environment (dev/staging/prod). Use when the user asks about: is service X running? what version is deployed? when was the last deployment? are there any active incidents? what's wrong with service Y?"

The difference:

  1. Specificity — lists exactly what the tool returns
  2. Trigger phrases — "Use when the user asks about..." gives the model explicit routing signals
  3. Examples — concrete sample questions help the model recognize when to invoke

Apply the same principle to parameter descriptions:

// Bad
@ToolParam("service name") String service

// Good
@ToolParam("The exact service name as it appears in the deployment system, e.g. 'payment-service', 'user-api', 'notification-worker'. If the user gives a partial name, use your best judgment.") String service
Enter fullscreen mode Exit fullscreen mode

Choosing the Right Platform

flowchart TD
    A{Where will\nusers invoke\nthe skill?}

    A -->|In GitHub Copilot Chat\nin their IDE| B[GitHub Copilot Extension]
    A -->|In Claude.ai or\nClaude Code| C{Persistent connection\nor per-call?}
    A -->|In your own app\nor API| D{Language / framework?}
    A -->|Any MCP client\nCursor, Zed, etc.| E[MCP Server]

    C -->|One-off, embedded\nin app logic| F[Claude Tool Use\nvia API directly]
    C -->|Persistent, IDE-like\nmulti-session| E

    D -->|Java / Spring Boot| G[Spring AI @Tool\nor MCP Server]
    D -->|Any other| H[Claude API tool_use\nin your language]

    style B fill:#1e3a5f,color:#7dd3fc
    style E fill:#065f46,color:#6ee7b7
    style G fill:#374151,color:#d1d5db
Use case Best platform
IDE assistant for developers (@your-team) GitHub Copilot Extension
App feature powered by Claude (summarize, analyze, assist) Claude Tools via API (tool_use)
Developer tool available in Claude Code / Cursor / Zed MCP Server
Team automation inside Claude Code sessions Claude Code Skill (SKILL.md)
Enterprise integration across multiple AI clients MCP Server (platform-agnostic)
Prototype / internal hackathon Claude Tools via API (fastest to build)

The Pattern That Works Across All Platforms

Regardless of which platform you choose, four implementation rules apply everywhere:

1. Return structured data, let the AI format it for humans.
Don't return "Payment service is running version 2.14.1 and was last deployed at 3:42 PM." Return { version: "2.14.1", lastDeployedAt: "2026-09-02T15:42:00Z", status: "healthy" }. The AI formats prose better than you do. Structured data also survives prompt changes without touching your implementation.

2. Fail loudly with context.

// Bad — Claude has nothing to work with
throw new RuntimeException("Not found");

// Good — Claude can explain this to the user
return new SkillError("SERVICE_NOT_FOUND",
    "No service named '" + service + "' found in " + environment + ". " +
    "Available services: payment-service, user-api, notification-worker");
Enter fullscreen mode Exit fullscreen mode

3. Add safety rails on destructive operations.
For tools like trigger_rollback, add a requirement in the description: "ONLY invoke after the user has explicitly confirmed they want to roll back, e.g. 'yes, roll it back' or 'proceed with rollback'." The model respects this instruction.

4. Keep tool count low.
More tools = more confusion about which one to invoke. Target 3–5 well-named tools rather than 12 narrowly-scoped ones. If you have more, group them into domain-specific servers/extensions.

Key Takeaways

  • GitHub Copilot Extensions are best for developer tools that live inside the IDE alongside code — your team can invoke them with @your-extension without leaving the editor.

  • Claude Tools (API) are best for embedding AI capabilities inside your own applications — an AI assistant inside your product, a chatbot, a document analyzer. You control the full stack.

  • MCP Servers are the most portable option — build once, run in any MCP-compatible client. This is the right choice for tools you want available everywhere your team uses AI.

  • Claude Code Skills are configuration, not code — great for standardizing team workflows and creating shared slash commands without a deployment.

  • The description is the routing logic. A well-written description is worth more than any amount of prompt engineering around the tool invocation. Invest here first.

  • Return structured data. Let the AI format results for humans. Your job is to return correct, complete data in a machine-readable format. The AI's job is to turn that into a clear, contextualized response.

The skills you build today will run in assistants your team doesn't know about yet. Invest 30 extra minutes on the schema and description — it compounds across every client that ever connects to your skill.

Top comments (0)