课程0基础Agent开发课 / Claude-Code实战教程 / MCP连接外部工具
— 28 min read

MCP连接外部工具

> **时效说明**:本文内容以 2026 年 3 月为基准。

Claude Code:MCP:连接外部工具

时效说明:本文内容以 2026 年 3 月为基准。

10.1 MCP——让Claude连接外部工具

MCP全称Model Context Protocol,是Anthropic推出的一个开放协议,定义了AI模型和外部工具之间怎么通信。你可以把它理解成AI世界的JDBC——JDBC定义了Java程序和数据库怎么通信,MCP定义了AI和各种外部服务怎么通信。

协议本身是开放的,已经有大量第三方实现了各种MCP Server:GitHub、PostgreSQL、MySQL、Sentry、Slack、Jira……这些Server暴露出来的能力,Claude可以直接调用。

MCP诞生之前,AI助手能做的事局限在它的上下文窗口里——你把代码贴进来,它帮你分析;你把报错贴进来,它帮你排查。它自己连不上任何外部系统。MCP改变了这个局面,让AI可以主动去拉数据、执行操作,变成一个真正意义上的AI代理。

10.2 MCP的工作原理

理解原理能帮你在遇到问题时更快定位。

10.2.1 Server/Client架构

MCP采用标准的Client/Server架构:

  • MCP Server:一个独立运行的进程,负责封装某个外部系统的能力(比如把GitHub API包成标准MCP工具)
  • MCP Client:Claude Code本身扮演Client角色,通过标准协议调用Server暴露的工具
  • 通信方式:支持两种——stdio(标准输入输出,本地进程间通信)和HTTP/SSE(网络通信,适合远程Server)

整个调用链是这样的:

code
你对Claude说话
  → Claude决定要调用某个外部工具
  → Claude Code(Client)通过MCP协议向Server发送工具调用请求
  → MCP Server收到请求,调用真实的外部API(GitHub/数据库/Slack等)
  → Server把结果返回给Claude Code
  → Claude看到结果,继续和你对话

10.2.2 工具是如何暴露的

每个MCP Server启动后,会向Claude Code注册它支持的工具列表。每个工具都有:

  • 名称:比如create_issuequery_database
  • 描述:用自然语言说明这个工具做什么,Claude根据描述判断什么时候调用
  • 参数定义:JSON Schema格式,指定需要哪些参数

Claude会把所有已连接Server的工具列表加入自己的上下文。当你问"帮我创建一个GitHub Issue"时,Claude看到有一个叫create_issue的工具,描述是"在GitHub仓库创建Issue",它就会调用这个工具。

10.2.3 什么时候用stdio,什么时候用HTTP

场景 推荐方式 原因
本地开发环境,MCP Server跑在本机 stdio 延迟最低,无网络开销
团队共享的MCP Server,部署在内网服务器 HTTP 多人共用,统一管理
第三方SaaS提供的MCP服务 HTTP Server不在你控制下
需要认证的企业内部系统 HTTP + Token 有完整的安全控制

大多数开源MCP Server(GitHub、数据库这些)都是stdio方式,通过npx启动一个本地进程。

10.3 为什么需要MCP

在没有MCP之前,你想让Claude帮你分析一个GitHub PR,流程是这样的:你自己去GitHub复制PR的diff,粘贴到对话里,Claude分析完给你建议,你再回去GitHub做操作。

有了MCP之后,你直接说"帮我看一下#234这个PR有什么问题,然后写一条review评论",Claude自己去拉PR内容,分析完直接调用GitHub API提交评论,整个过程你不用动手。

对Java后端开发者来说,最有价值的几个连接是:

  • GitHub:代码仓库操作(PR review、Issue管理、代码搜索)
  • 数据库:直接查生产数据,分析问题
  • Sentry:直接看报错堆栈,不用复制粘贴
  • Jira:查看任务状态、更新进度
  • Slack:通知机器人、查看频道消息

这几个覆盖了日常排查问题和项目管理80%的信息需求。

10.4 配置方式

10.4.1 命令行快速添加

命令行添加是最快的方式,适合个人本地使用:

bash
# 添加GitHub MCP(通过npx运行)
claude mcp add github -- npx @modelcontextprotocol/server-github

# 添加MySQL MCP
claude mcp add mysql -- npx @modelcontextprotocol/server-mysql

# 添加HTTP类型的MCP
claude mcp add --transport http myserver https://mcp.example.com

# 查看已添加的MCP列表
claude mcp list

# 删除某个MCP
claude mcp remove github

命令行配置只存在你本地(存储在~/.claude/settings.json),团队协作的时候更推荐用项目级配置文件.mcp.json,提交到git仓库,所有人共享同一套工具配置。

10.4.2 .mcp.json完整配置示例

将以下配置文件放在项目根目录,命名为.mcp.json

GitHub + MySQL + Sentry基础配置:

json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
      }
    },
    "mysql": {
      "command": "npx",
      "args": ["-y", "@benborla29/mcp-server-mysql"],
      "env": {
        "MYSQL_HOST": "localhost",
        "MYSQL_PORT": "3306",
        "MYSQL_USER": "readonly_user",
        "MYSQL_PASSWORD": "${DB_PASSWORD}",
        "MYSQL_DATABASE": "myapp_prod",
        "MYSQL_ALLOW_INSERT_OPERATION": "false",
        "MYSQL_ALLOW_UPDATE_OPERATION": "false",
        "MYSQL_ALLOW_DELETE_OPERATION": "false"
      }
    },
    "sentry": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-sentry"],
      "env": {
        "SENTRY_AUTH_TOKEN": "${SENTRY_TOKEN}",
        "SENTRY_ORG": "your-org-slug"
      }
    }
  }
}

10.5 更多MCP Server配置示例

10.5.1 PostgreSQL配置

PostgreSQL有官方维护的MCP Server,连接生产库时务必用只读账号:

json
{
  "mcpServers": {
    "postgresql": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://readonly_user:${PG_PASSWORD}@localhost:5432/myapp_prod"
      }
    }
  }
}

连接好之后可以直接问Claude:"帮我查一下users表里最近7天注册但是没有完成邮箱验证的用户数量",Claude会直接执行SQL并返回结果。

PostgreSQL MCP Server暴露的主要工具:

  • query:执行只读SQL查询
  • list_tables:列出数据库里的所有表
  • describe_table:查看某个表的字段定义

10.5.2 Redis配置

Redis的MCP Server可以让Claude直接操作缓存,对排查缓存相关问题很有用:

json
{
  "mcpServers": {
    "redis": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-redis"],
      "env": {
        "REDIS_URL": "redis://:${REDIS_PASSWORD}@localhost:6379/0"
      }
    }
  }
}

典型使用场景:"帮我看一下缓存里session开头的key有多少个,内存占用大概是多少"、"这个用户的购物车数据在Redis里是什么结构"。

生产Redis建议:

  1. 只给KEYSGETTYPETTL等只读命令的权限
  2. 不要给FLUSHDBDEL等破坏性命令权限
  3. 如果Redis没有密码,强烈建议先加上,不要裸连

10.5.3 Jira配置

Jira的MCP Server让Claude可以直接查看和操作项目任务:

json
{
  "mcpServers": {
    "jira": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-jira"],
      "env": {
        "JIRA_URL": "https://your-company.atlassian.net",
        "JIRA_EMAIL": "your-email@company.com",
        "JIRA_API_TOKEN": "${JIRA_TOKEN}"
      }
    }
  }
}

Jira API Token在Atlassian账户设置里生成,不是密码。有了这个配置之后:

  • "帮我列一下本周还没关闭的Bug,按优先级排序"
  • "把PROJ-456的状态改成In Progress,并加一条评论说正在处理"
  • "给这次sprint生成一个进度报告"

Claude都能直接操作。

10.5.4 Slack配置

Slack MCP Server可以让Claude查看频道消息、发送通知:

json
{
  "mcpServers": {
    "slack": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-slack"],
      "env": {
        "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
        "SLACK_TEAM_ID": "T01234567"
      }
    }
  }
}

Slack Bot Token在Slack App管理后台创建。权限建议只开:

  • channels:read:读取频道列表
  • chat:write:发送消息
  • messages:read:读取消息(可选)

不要开files:writeadmin.*这些高权限。

典型场景:"部署完成后自动发一条消息到#backend-deploys频道"、"帮我查一下上周#backend频道里有没有关于数据库超时的讨论"。

10.5.5 本地文件系统(特殊权限目录)

有时候你需要让Claude访问某个特定的目录,比如你的文档库或者配置文件仓库,但不想给它整个文件系统权限:

json
{
  "mcpServers": {
    "docs-library": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/you/company-docs",
        "/Users/you/api-specs"
      ]
    }
  }
}

@modelcontextprotocol/server-filesystem接受多个路径参数,Claude只能访问这些目录,访问其他路径会被拒绝。适合把公司内部文档库挂进来,让Claude能查阅文档来回答问题。

10.6 自己写一个简单的MCP Server

第三方MCP Server不一定覆盖你的所有需求。如果你有公司内部的API(比如自研的部署平台、监控系统),可以自己写一个MCP Server把它暴露给Claude。

下面是一个用Python写的MCP Server示例,暴露了公司内部部署平台的两个接口。

10.6.1 安装依赖

bash
pip install mcp httpx

mcp是官方Python SDK,httpx是异步HTTP客户端。

10.6.2 完整MCP Server代码

python
#!/usr/bin/env python3
"""
公司内部部署平台的MCP Server
暴露两个工具:
- get_deploy_status:查看某个服务的部署状态
- trigger_deploy:触发一次部署
"""
import asyncio
import json
from typing import Any
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

# 公司内部部署平台的API地址(从环境变量读取,不硬编码)
import os
DEPLOY_API_BASE = os.environ.get("DEPLOY_API_BASE", "https://deploy.internal.company.com")
DEPLOY_API_TOKEN = os.environ.get("DEPLOY_API_TOKEN", "")

# 创建MCP Server实例
server = Server("deploy-platform")

@server.list_tools()
async def list_tools() -> list[types.Tool]:
    """向Claude注册这个Server提供的工具列表"""
    return [
        types.Tool(
            name="get_deploy_status",
            description=(
                "查询指定服务的最新部署状态,包括部署时间、部署人、版本号、当前状态(成功/失败/进行中)。"
                "当用户问某个服务的部署情况、上线状态、当前版本时调用。"
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "service_name": {
                        "type": "string",
                        "description": "服务名称,例如 user-service、order-service"
                    },
                    "environment": {
                        "type": "string",
                        "enum": ["dev", "staging", "prod"],
                        "description": "环境,默认是prod",
                        "default": "prod"
                    }
                },
                "required": ["service_name"]
            }
        ),
        types.Tool(
            name="trigger_deploy",
            description=(
                "触发指定服务的部署。会用指定的镜像版本或者Git Tag发起一次部署。"
                "注意:只能部署到dev和staging环境,prod环境需要在部署平台手动确认。"
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "service_name": {
                        "type": "string",
                        "description": "要部署的服务名称"
                    },
                    "version": {
                        "type": "string",
                        "description": "镜像Tag或者Git Tag,例如 v1.2.3 或 latest"
                    },
                    "environment": {
                        "type": "string",
                        "enum": ["dev", "staging"],
                        "description": "目标环境(只支持dev和staging)"
                    },
                    "reason": {
                        "type": "string",
                        "description": "部署原因,会记录在部署日志里"
                    }
                },
                "required": ["service_name", "version", "environment"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
    """处理Claude发来的工具调用请求"""

    if name == "get_deploy_status":
        return await _get_deploy_status(
            service_name=arguments["service_name"],
            environment=arguments.get("environment", "prod")
        )

    elif name == "trigger_deploy":
        return await _trigger_deploy(
            service_name=arguments["service_name"],
            version=arguments["version"],
            environment=arguments["environment"],
            reason=arguments.get("reason", "Claude触发的部署")
        )

    else:
        return [types.TextContent(
            type="text",
            text=f"未知工具:{name}"
        )]

async def _get_deploy_status(service_name: str, environment: str) -> list[types.TextContent]:
    """调用部署平台API查询状态"""
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(
                f"{DEPLOY_API_BASE}/api/v1/deployments/{service_name}",
                params={"env": environment},
                headers={"Authorization": f"Bearer {DEPLOY_API_TOKEN}"},
                timeout=10.0
            )
            response.raise_for_status()
            data = response.json()

            # 格式化成易读的文本返回给Claude
            result = (
                f"服务:{service_name}{environment} 环境)\n"
                f"状态:{data['status']}\n"
                f"版本:{data['version']}\n"
                f"部署时间:{data['deployed_at']}\n"
                f"部署人:{data['deployed_by']}\n"
                f"提交信息:{data.get('commit_message', 'N/A')}"
            )

        except httpx.HTTPStatusError as e:
            result = f"查询失败:HTTP {e.response.status_code}{e.response.text}"
        except httpx.TimeoutException:
            result = "查询超时,部署平台可能暂时不可用"
        except Exception as e:
            result = f"查询出错:{str(e)}"

    return [types.TextContent(type="text", text=result)]

async def _trigger_deploy(
    service_name: str,
    version: str,
    environment: str,
    reason: str
) -> list[types.TextContent]:
    """调用部署平台API触发部署"""

    # 安全检查:不允许部署到prod
    if environment == "prod":
        return [types.TextContent(
            type="text",
            text="[拒绝] prod环境部署需要在部署平台手动确认,无法通过API触发。"
        )]

    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(
                f"{DEPLOY_API_BASE}/api/v1/deployments",
                json={
                    "service": service_name,
                    "version": version,
                    "environment": environment,
                    "reason": reason
                },
                headers={"Authorization": f"Bearer {DEPLOY_API_TOKEN}"},
                timeout=30.0
            )
            response.raise_for_status()
            data = response.json()

            result = (
                f"部署已触发!\n"
                f"部署ID:{data['deploy_id']}\n"
                f"服务:{service_name}{environment}\n"
                f"版本:{version}\n"
                f"预计完成时间:{data.get('estimated_duration', '3-5分钟')}\n"
                f"查看进度:{DEPLOY_API_BASE}/deployments/{data['deploy_id']}"
            )

        except Exception as e:
            result = f"部署触发失败:{str(e)}"

    return [types.TextContent(type="text", text=result)]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options()
        )

if __name__ == "__main__":
    asyncio.run(main())

10.6.3 把自定义Server加入.mcp.json

json
{
  "mcpServers": {
    "deploy-platform": {
      "command": "python3",
      "args": ["/path/to/your/deploy_mcp_server.py"],
      "env": {
        "DEPLOY_API_BASE": "https://deploy.internal.company.com",
        "DEPLOY_API_TOKEN": "${DEPLOY_TOKEN}"
      }
    }
  }
}

配置好之后,Claude就能直接说"帮我把user-service的v1.2.3部署到staging",它会调用你的MCP Server,Server转发到公司内部API,真实地触发一次部署。

10.6.4 description字段的重要性

工具描述(description字段)直接决定Claude什么时候会调用这个工具。写得好,Claude能准确识别时机;写得差,Claude不知道什么时候用,或者用错。

好的description应该包含:

  1. 工具的核心功能(一句话)
  2. 什么时候应该调用(关键词举例)
  3. 什么时候不该调用(避免误触发)
python
# 差的描述(太模糊)
description="Gets deployment info"

# 好的描述(明确触发场景)
description=(
    "查询指定服务的最新部署状态,包括部署时间、部署人、版本号、当前状态。"
    "当用户询问某个服务是否已上线、最新版本是什么、谁部署的时调用。"
    "不要用于查询代码本身,那应该用GitHub工具。"
)

10.7 MCP工具调用的完整交互流程

了解完整的调用链,遇到问题时能更快定位。

场景:你说"帮我查一下user-service现在生产环境是哪个版本"

  1. Claude收到你的问题,分析上下文
  2. Claude查看当前连接的MCP Server工具列表,发现deploy-platform下有个get_deploy_status工具,描述里提到"查询服务当前版本"
  3. 弹出确认提示(默认行为):
    code
    Claude想要使用工具:get_deploy_status
    参数:{"service_name": "user-service", "environment": "prod"}
    [允许] [拒绝] [本次会话全部允许]
  4. 你点击"允许"
  5. Claude Code把工具调用请求发给deploy-platform MCP Server
  6. MCP Server调用部署平台API,拿到结果
  7. 结果返回给Claude:"user-service,prod环境,版本v1.5.2,2026-03-28 14:30部署,部署人:张三"
  8. Claude把这个信息整合进回复,告诉你

整个流程对你来说是透明的,但了解它能帮你在步骤5-6卡住时知道去哪里排查。

10.8 MCP的安全最佳实践

10.8.1 最小权限原则

每个MCP Server只给它需要的最小权限,不要图方便给一个管理员Token什么都能干。

json
{
  "mcpServers": {
    "github": {
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
      }
    }
  }
}

GitHub Token的权限范围选择:

  • 只需要读PR、Issue:开read:repo
  • 需要提交评论:开write:discussion
  • 不要开delete_repoadmin:org这些危险权限

数据库同理,生产库只给只读账号:

sql
-- 为Claude专门创建只读账号
CREATE USER 'claude_readonly'@'localhost' IDENTIFIED BY '${password}';
GRANT SELECT ON myapp_prod.* TO 'claude_readonly'@'localhost';
-- 不要给INSERT、UPDATE、DELETE、DROP权限
FLUSH PRIVILEGES;

10.8.2 敏感数据处理

有几类数据要特别小心:

密码和个人信息:如果数据库里有password、phone、id_card这些字段,在MCP Server层面做脱敏,不要让它们原样传给Claude。

python
# 在自定义MCP Server里做脱敏
def mask_sensitive_fields(row: dict) -> dict:
    sensitive_keys = {'password', 'phone', 'id_card', 'bank_account', 'email'}
    return {
        k: '***MASKED***' if k.lower() in sensitive_keys else v
        for k, v in row.items()
    }

大量数据:让Claude一次查几百万条数据既没意义又危险,在MCP Server里限制查询结果数量:

python
# 强制限制SELECT查询最多返回1000行
def safe_query(sql: str) -> str:
    sql = sql.strip().rstrip(';')
    if 'LIMIT' not in sql.upper():
        sql += ' LIMIT 100'
    return sql

只读原则:数据库MCP Server除非有明确需求,否则只开只读工具。写操作(INSERT/UPDATE/DELETE)要显式声明,在工具描述里说明,方便Claude在决策时更谨慎。

10.8.3 审计日志

所有MCP工具调用都应该有日志,方便事后审计。可以在自定义MCP Server里集中记录:

python
import logging
from datetime import datetime

# 配置审计日志
audit_logger = logging.getLogger('mcp.audit')
audit_logger.setLevel(logging.INFO)
handler = logging.FileHandler('/var/log/mcp-audit.log')
handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
audit_logger.addHandler(handler)

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    # 记录每一次工具调用
    audit_logger.info(json.dumps({
        "timestamp": datetime.now().isoformat(),
        "tool": name,
        "arguments": _redact_sensitive(arguments),  # 脱敏后记录
        "caller": "claude-code"
    }))

    # 正常处理...

日志格式用JSONL,方便用jq查询:

bash
# 查看今天所有trigger_deploy调用
jq 'select(.tool == "trigger_deploy")' /var/log/mcp-audit.log

# 统计今天数据库查询次数
jq 'select(.tool == "query") | .timestamp' /var/log/mcp-audit.log | wc -l

10.8.4 .mcp.json的安全处理

.mcp.json会提交到git仓库,所以:

  1. 绝对不要在这个文件里写明文密码,全部用${ENV_VAR}引用
  2. 把密钥存在本地的.env文件,加到.gitignore
  3. CI/CD环境通过Secrets管理(GitHub Secrets、Vault等)注入
bash
# .env文件(加到.gitignore)
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
DB_PASSWORD=your_password_here
DEPLOY_TOKEN=your_token_here
bash
# 加载.env后启动Claude
source .env && claude
# 或者用direnv自动加载:https://direnv.net/

10.9 MCP调试:排查连接失败

10.9.1 常见错误和解决方法

错误1:MCP Server启动失败

症状:Claude提示"无法连接到 xxx MCP Server"

排查步骤:

bash
# 直接在终端手动启动MCP Server,看有没有报错
npx @modelcontextprotocol/server-github

如果报错,常见原因:

  • npx没装或版本太旧:npm install -g npx
  • 网络问题,npx无法下载包:检查代理设置
  • 环境变量没设置:检查GITHUB_PERSONAL_ACCESS_TOKEN是否存在

错误2:工具调用返回权限错误

症状:Claude调用工具,返回"401 Unauthorized"或"403 Forbidden"

排查步骤:

bash
# 直接测试Token是否有效
curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user

如果返回用户信息,Token有效但权限不足;如果返回401,Token本身失效了,需要重新生成。

错误3:.mcp.json配置不生效

症状:Claude完全不知道MCP工具的存在

排查步骤:

bash
# 验证JSON语法
python3 -c "import json; json.load(open('.mcp.json')); print('JSON语法正确')"

# 确认文件在项目根目录
ls -la .mcp.json

# 重启Claude Code(配置文件在启动时读取,改了之后需要重启)

错误4:自定义Python MCP Server没响应

在自定义Server的main()里加日志,看是否正常接收到请求:

python
async def main():
    import sys
    print("MCP Server 启动中...", file=sys.stderr)
    async with stdio_server() as (read_stream, write_stream):
        print("等待连接...", file=sys.stderr)
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options()
        )

注意:调试信息必须输出到stderr,不能输出到stdout(stdout是MCP协议通信用的)。

10.9.2 MCP Server的本地测试

在没有Claude的情况下测试你的MCP Server是否正常工作:

bash
# 安装MCP Inspector工具
npm install -g @modelcontextprotocol/inspector

# 启动Inspector,它会连接你的MCP Server并提供调试界面
mcp-inspector python3 /path/to/your_server.py

Inspector会在本地起一个Web界面,你可以在里面手动调用工具,验证返回值,不需要依赖Claude来触发。


本页目录