Claude Code and Agent Skills: MCP in the Era of Autonomous Coding

Claude Code is Anthropic’s autonomous coding agent – and it is built on MCP. When Claude Code reads files, runs tests, executes commands, and browses documentation, it does all of this through MCP servers. The architecture is not a coincidence: it is Anthropic demonstrating exactly how a production-grade autonomous agent should integrate with external systems. Understanding how Claude Code uses MCP is one of the fastest ways to understand how you should build your own agents.

Claude Code architecture diagram showing MCP servers for filesystem bash tools web browser connected to Claude agent dark
Claude Code is an MCP host: it connects to filesystem, bash, and browser servers, and orchestrates them with Claude 3.5.

Claude Code’s MCP Architecture

Claude Code (the CLI tool, claude) operates as an MCP host. When it starts, it connects to a set of built-in MCP servers that provide its core capabilities: computer-use (screen reading/clicking), bash (shell command execution), and files (filesystem read/write). You can extend Claude Code with your own custom MCP servers, making it immediately capable of working with your specific project tools.

# ~/.claude/config.json - Extend Claude Code with your MCP servers
{
  "mcpServers": {
    "my-project-tools": {
      "command": "node",
      "args": ["./tools/mcp-server.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost/mydb"
      }
    },
    "github-tools": {
      "command": "npx",
      "args": ["-y", "@anthropic-ai/mcp-github"]
    }
  }
}

Once configured, Claude Code can use your custom server’s tools as naturally as it uses its built-in bash or filesystem tools. Your create_github_issue tool becomes as usable as Bash(git commit).

Building Agent Skills for Claude Code

The most powerful Claude Code extension pattern is the “agent skill” – a specialised MCP server that encapsulates a complex workflow as a single callable tool. Instead of Claude figuring out the 20-step process to deploy a microservice, you encode those steps in a deploy_service tool that handles all the complexity.

// deploy-skill-server.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const exec = promisify(execFile);
const server = new McpServer({ name: 'deploy-skills', version: '1.0.0' });

server.tool(
  'deploy_service',
  `Deploy a microservice to Kubernetes. Handles build, push, and rollout.
  Returns the deployment status and the new pod count.`,
  {
    service_name: z.string().describe('Name of the service to deploy'),
    image_tag: z.string().describe('Docker image tag to deploy'),
    namespace: z.string().default('production').describe('Kubernetes namespace'),
    replicas: z.number().int().min(1).max(10).default(2),
  },
  async ({ service_name, image_tag, namespace, replicas }) => {
    const steps = [];

    // Step 1: Build
    const { stdout: buildOut } = await exec('docker', [
      'build', '-t', `${service_name}:${image_tag}`, './services/' + service_name
    ]);
    steps.push('Build: OK');

    // Step 2: Push
    await exec('docker', ['push', `myregistry/${service_name}:${image_tag}`]);
    steps.push('Push: OK');

    // Step 3: Deploy
    const manifest = generateK8sManifest(service_name, image_tag, namespace, replicas);
    await exec('kubectl', ['apply', '-f', '-'], { input: manifest });
    steps.push(`Deploy: OK (${replicas} replicas)`);

    // Step 4: Wait for rollout
    await exec('kubectl', ['rollout', 'status', `deployment/${service_name}`, '-n', namespace]);
    steps.push('Rollout: Complete');

    return {
      content: [{ type: 'text', text: steps.join('\n') + `\n\nService ${service_name} deployed successfully.` }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
Claude Code agent skills diagram showing complex deployment workflow encapsulated as single MCP tool dark
Agent skills: encode complex multi-step workflows as atomic MCP tools. Claude calls one tool instead of orchestrating ten.

Permission Modes in Claude Code

Claude Code has a permission system that controls what actions it can take without asking for confirmation. MCP tools are subject to the same permission model. You can configure Claude Code to auto-approve specific tools, require confirmation for destructive operations, or run in fully supervised mode.

# .claude/settings.json (project-level)
{
  "permissions": {
    "allow": [
      "Bash(git *)",              # Allow all git commands
      "mcp:my-project-tools:read_*",  # Allow read-only tools from my server
      "Read(**)"                   # Allow reading any file
    ],
    "deny": [
      "mcp:my-project-tools:deploy_*",  # Always ask before deploying
      "Bash(rm -rf *)"            # Never auto-approve recursive deletes
    ]
  }
}

“Claude Code is designed to be an autonomous coding agent that can understand and work on complex codebases. It uses a set of built-in tools and can be extended with custom MCP servers to access domain-specific capabilities.” – Anthropic Documentation, Claude Code

Failure Modes with Claude Code MCP Extensions

Case 1: Tools That Are Too Granular

// BAD: Too granular - Claude has to call many tools in sequence and may make mistakes
server.tool('set_k8s_namespace', '...', { ns: z.string() }, handler);
server.tool('set_k8s_image', '...', { image: z.string() }, handler);
server.tool('apply_k8s_manifest', '...', { manifest: z.string() }, handler);
server.tool('watch_k8s_rollout', '...', { deployment: z.string() }, handler);

// BETTER: One atomic skill tool that handles the whole workflow
server.tool('deploy_service', 'Deploy a service to k8s...', { service: z.string(), ... }, handler);

Case 2: Forgetting to Handle Long-Running Operations

// Build + deploy can take minutes
// Don't timeout. Stream progress via notifications or use progress indicators
// Claude Code will wait, but it needs feedback to know the tool is running

server.tool('build_and_deploy', '...', { ... }, async ({ service }) => {
  // Send progress
  process.stderr.write(`[build] Starting build for ${service}...\n`);
  await buildService(service);  // May take 2-10 minutes
  process.stderr.write(`[deploy] Deploying ${service}...\n`);
  await deployService(service);
  return { content: [{ type: 'text', text: 'Done.' }] };
});

What to Check Right Now

  • Install Claude Codenpm install -g @anthropic-ai/claude-code. Then run claude in a project directory to see it in action.
  • Add your MCP server to Claude Code config – add it to ~/.claude/config.json or .claude/config.json (project-level). Then run claude and ask it to use your tool.
  • Design tools as atomic workflows – each tool should complete one meaningful unit of work end-to-end. Avoid exposing low-level implementation details as separate tools.
  • Review the permission system – set appropriate allow and deny rules for your project. Deny destructive tools by default and require explicit confirmation.

nJoy 😉

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.