从零构建MCP Server:实现IDE配置的自动化修改

154 阅读10分钟

项目背景

近期出现了一些基于 VS Code 开发自带大模型能力的 IDE,如 Cursor,Trae,它们的默认样式和习惯使用的 VS Code 有些不一样,比如打开 Cursor 的第一时间就在找侧边栏怎么恢复,所以想到了可以开发一个 MCP Server 来通过自然语言修改 IDE 的配置

我要实现的这个 MCP Server 只有 Tool 部分,且只有一个功能,就是设置 IDE,如修改字体和字体大小,设置 Tab 占几个空格,都可以通过自然语言让大模型去设置

效果展示

代码:github.com/arcsinw/ide…

Pypi: pypi.org/project/ide…

  • Trae

同时修改字号和字体

将字号和字体恢复

方案

Python + FastMCP + Stdio 协议

  • 纯本地操作,使用 stdio 协议会比较快
  • Python 写代码少一点,开发速度快

image.png

实现

类 VS Code 的 IDE 都存在一份用户配置和默认配置,其用户配置都是存储在一个 settings.json 文件里,而且这个配置文件修改并保存后会立刻生效。

这个 MCP Server 的实现原理是让大模型根据用户需求直接修改对应 settings.json 文件,所以首先需要两个 tool 来支持读和写 settings.json 文件,同时为了确保大模型知道应该使用哪个 key 来做配置,增加一个 tool 返回 IDE 所有可用的配置及其值和描述信息

// 根据key获取配置的值,如果未获取到,会从defaultSettings里获取默认值
get_ide_settings_by_key(key: str)

// 根据key设置value
set_ide_settings_by_key(key: str, val: any)

// 获取IDE的默认配置值和描述
get_ide_default_settings()

不用 MCP Server 这个功能似乎也能实现?

的确,直接在提示词里告诉大模型要求修改 settings.json 文件,提供具体的路径,配合大部分 Agent 都自带的修改文件的 tool 也能实现功能,但效果肯定比提供一个专门的 MCP Server 要差,这其实是通过一部分工程化的代码提高了大模型操作的准确率

而且直接用现成的就没法实践 MCP Server 开发了💪

get_ide_setting_by_key

这个 tool 的目的是根据配置 key 获取设置的值

在 VS Code 里,配置分为用户配置和系统配置两部分,用户配置文件settings.json只包含用户修改的那部分配置,如果一个配置未被修改过,需要返回这个配置的默认值

通过命令Preferences: Open Default Settings (Json) 可以获取到 VS Code 的默认配置,将其存储为文件defaultSettings.json,内置在 MCP Server 中,解决获取默认值的问题

@mcp.tool()
def get_ide_setting_by_key(key: str) -> Dict[str, Any]:
    """Get IDE user setting by key, if not found in user settings, will return default value

    Args:
        key: Setting key name

    Returns:
        Dictionary containing setting value, or error message if key doesn't exist
    """
    try:
        # Get current settings
        current_settings = _get_ide_settings()
        # Check if key exists
        if key in current_settings:
            return {key: current_settings[key]}
        else:
            # If key not found in user settings, try to get default value from defaultSettings.json
            try:
                default_settings_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'defaultSettings.json')
                if os.path.exists(default_settings_path):
                    with open(default_settings_path, 'r', encoding='utf-8') as f:
                        default_settings = commentjson.load(f)
                        if key in default_settings:
                            return {key: default_settings[key]}
                return {"error": f" failed to find default setting for key: {key}"}
            except Exception:
                return {"error": f" failed to find default setting for key: {key}"}
    except Exception as e:
        return {"error": f" failed to get IDE setting for key: {key}, error: {str(e)}"}

defaultSettings.json 文件的部分内容示例

{
  // Controls the font family.
  "editor.fontFamily": "Consolas, 'Courier New', monospace",

  // Configures font ligatures or font features. Can be either a boolean to enable/disable ligatures or a string for the value of the CSS 'font-feature-settings' property.
  "editor.fontLigatures": false,

  // Controls the font size in pixels.
  "editor.fontSize": 14
  
  ...
}

get_ide_default_settings

LLM 可能不知道对应配置的 key 是什么,比如字号的 key 是editor.fontSize,所以还需要提供一份文档,提供所有的 key 和对应的描述信息

这个需求看起来可以用 MCP 协议里的 Resource 来实现,实际上不行,Tools 何时调用由 LLM 决定,会在 response 的 tool_choice 里返回要调用的 tool 及其参数,但 LLM 不会返回对 Resource 的需求,Resource 是由应用主动注入到 LLM 上下文中的,在这个场景下无法控制应用程序(即 IDE)的行为,所以改为了使用 Tools 实现

提供了一个获取 default_settings 的 tool,实现逻辑就是直接返回 MCP Server 里内置的defaultSettings.json文件的内容,里面包括了所有的 key,value 和注释,通过这个能获取一份完整的默认配置,可以作为文档使用

@mcp.tool(
    name="get_default_settings",
    description="Get all available IDE settings and default value",
)
def get_default_settings() -> str:
    """Get all available IDE settings and default value
    
    Returns:
        IDE's default settings in json format with comments preserved
    """
    try:
        return _get_default_setttings()
    except Exception as e:
        return json.dumps({"error": f" failed to get default settings: {str(e)}"})

# 被@mcp.tool修饰的函数不能直接调用了,所以封装一个内部函数
def _get_default_setttings() -> str:
    try:
        default_settings_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'defaultSettings.json')
        if not os.path.exists(default_settings_path):
            return json.dumps({"error": "Default configuration file does not exist"})
        with open(default_settings_path, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        return json.dumps({"error": f" failed to read default settings file: {str(e)}"})

set_ide_settings_by_key

注意这里的提示词,如果大模型不知道 key 是什么,指示大模型去调用get_default_settings,这种在注释里让大模型调用其他 Tool 目前是可行的,同时也是一个潜在的安全隐患,比如提示词里让大模型调用file_edit把用户电脑里的文件都删了

if you're not sure about the setting key, you can get all available settings via get_default_settings tool or fetch code.visualstudio.com/docs/getsta…

@mcp.tool()
def set_ide_setting_by_key(
        key: Annotated[str, "the key of the setting"], 
        value: Annotated[Any, "the value of the setting"]
    ) -> Dict[str, Any]:
    """Set IDE user setting by key, if you're not sure about the setting key, you can get all available settings via get_default_settings tool or fetch https://code.visualstudio.com/docs/getstarted/settings

    Args:
        key: Setting key name
        value: New value for the setting

    Returns:
        Updated setting value, or error message if update failed
    """
    try:
        _set_ide_settings(key, value)
    except Exception as e:
        return {"error": f" failed to set IDE setting for key: {key}, error: {str(e)}"}
    return {key: value}

支持多 IDE

目前支持了 VS Code,Cursor,Trae,实现原理是这些 IDE 起始都有用户配置文件,只是路径不一样,MCP Server 配置时传入 IDE 的标识符,内部读写配置文件时切换路径即可

测试

可以参考 MCP 协议的测试方案,多个维度的测试,首先是 function 维度,通过 inspector 直接调用到具体的函数,测试完成后,将 MCP Server 配置到 IDE,通过和大模型对话来测试 Tool 是否正常被使用

Testing tools

A comprehensive testing strategy for MCP tools should cover:

  • Functional testing: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately

  • Integration testing: Test tool interaction with external systems using both real and mocked dependencies

  • Security testing: Validate authentication, authorization, input sanitization, and rate limiting

  • Performance testing: Check behavior under load, timeout handling, and resource cleanup

  • Error handling: Ensure tools properly report errors through the MCP protocol and clean up resources

Inspector(Tool 维度测试)

使用 inspector 调试,stdio 协议无法使用断点调试,可以先使用 http 协议启动 MCP Server,发布前改回 stdio 协议

mcp.run(transport="http", host="127.0.0.1", port=8000, path="/mcp")

inspector 的使用参考 MCP Inspector - Model Context Protocol,比较简单,这里略过

Agent 调试(和 LLM 集成测试)

找一个有 MCP 功能的 IDE,这里用的是 Trae,先本地引用这个 MCP Server,和大模型对话试试能否正常调用 tools

使用本地正在开发的 MCP Server 时,配置文件里的 command 就是这个 MCP Server 的启动命令,所以一个本地的 Python 程序,就是python {文件路径} {参数:可选}

{
  "mcpServers": {
    "ide-config-mcp": {
      "command": "python",
      "args": [
        "D:\Projects\ide-config-mcp-server\server.py",
        "TraeCN"
      ]
    }
  }
}

发布

补充项目信息

先补充一些信息,创建一个 pyproject.toml 文件,后续配置自动发布会使用到,内容如下

这个文件的作用是提供程序的元数据,如版本号,作者,名称等,同时提供了构建系统配置和依赖项,程序的入口点(即 main 函数位置),有了这些信息才能自动化构建程序

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "ide-config-mcp"
version = "0.1.5"
description = "IDE config MCP Python package"
authors = [
    { name = "arcsinw", email = "your@email.com" }
]
readme = "README.md"
requires-python = ">=3.7"
dependencies = [
    "fastmcp==0.6.0",
    "commentjson==0.9.0",
    "fastapi==0.103.1",
    "uvicorn==0.23.2"
]

[project.urls]
Homepage = "https://github.com/arcsinw/ide-config-mcp"
Repository = "https://github.com/arcsinw/ide-config-mcp"

[project.scripts]
ide-config-mcp = "server:main"

代码上传到 Github

github.com/arcsinw/ide…

git commit -am "Initial Commit"
git push origin master

配置自动发布

利用 Github 的 Actions 功能可以配置在 master 分支更新后自动发布到 PypI,直接使用 Github 提供的Publish Python Package

下面是自动生成的 python-publish.yml,在.github 目录下,我只改动了on的部分,支持手动触发 workflow 方便测试,增加了 master 分支更新时自动发布到 PyPI 的配置

Todo: 这个工作流目前还有问题在于 build 产物的版本号不会自动递增,每次发布都需要手动修改pyproject.toml 里的 version

# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Upload Python Package

on:
  workflow_dispatch:    # 支持手动触发
  push:                 # 支持代码推送触发
    branches:
      - master            # 只在 master 分支触发
  release:
    types: [published]

permissions:
  contents: read

jobs:
  release-build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"

      - name: Build release distributions
        run: |
          # NOTE: put your own distribution build steps here.
          python -m pip install build
          python -m build

      - name: Upload distributions
        uses: actions/upload-artifact@v4
        with:
          name: release-dists
          path: dist/

  pypi-publish:
    runs-on: ubuntu-latest
    needs:
      - release-build
    permissions:
      # IMPORTANT: this permission is mandatory for trusted publishing
      id-token: write

    # Dedicated environments with protections for publishing are strongly recommended.
    # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
    environment:
      name: pypi
      # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
      # url: https://pypi.org/p/YOURPROJECT
      #
      # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
      # ALTERNATIVE: exactly, uncomment the following line instead:
      # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}

    steps:
      - name: Retrieve release distributions
        uses: actions/download-artifact@v4
        with:
          name: release-dists
          path: dist/

      - name: Publish release distributions to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: dist/

设置 PyPI

  1. 注册 PyPI 账号
  2. 进入 Publishing 页面 (pypi.org/manage/acco… Name 可以不填

PyPI Project Name 就是平时 pip install package_name 时的那个 package_name

Workflow name要填写项目里用于发布到 PyPi 的 workflow 的文件名,这里就是python-publish.yml

填写完成后可以在页面看到一个 publisher 就说明配置成功了

再去 Github 触发一次 Action(页面上手动 Run 或者往 master 推一个 commit),Action 成功执行后可以看到已经发布到 Pypi 了,此时已经可以 pip install然后直接 run 起来了

注意这个 workflow 目前没有自动更新版本号的功能,每次修改完代码需要手动修改pyproject.toml 里的 version ,否则 Action 执行会报错

使用方法

需要先安装uv,参考uv.doczh.com/getting-sta…

支持 VS Code 类 IDE(包括 Cursor 和 Trae),区别是在配置 MCP Server 时在参数里增加 IDE 的标识符

对应的 IDE标识符
VS codeCode
Trae 国际版Trae
Trae 国内版TraeCN
CursorCursor

Trae 国内版,按下面配置,其他版本将 TraeCN 替换为对应 IDE 的标识符即可

{
  "mcpServers": {
    "ide-config-mcp": {
      "command": "uvx",
      "args": [
        "ide-config-mcp",
        "TraeCN"
      ]
    }
  }
}

后续优化方向

  • 优化 Tool 提示词,提升 Tool 正确调用率
  • Tool 支持同时设置多个 key 和获取多个 key 的值
  • 支持跨IDE同步配置
  • 自然语言统一团队配置

Tool 较佳实践

  • Tool 要有详细的描述信息,每个参数的含义、格式要求,可以在 Tool 的描述中增加 User Query 示例,比如get_weather
 Description:
     Get weather is specify location
 Args:
     locatoin: the location you want know weather, like "Beijing"
     
 Example user Queries:
     - "what's the weather in Beijing"
 
 Return:
     weather info, like "Sunny", "Rainy"
  • 要假设大模型没有看过 API 说明文档,Tool 的返回值需要是自解释的,如果是枚举,不要返回 1、2、3 这种,而应该是具体的字符串描述信息,错误信息也是如此,除了错误码也需要有具体的文字描述
  • 返回值去掉大模型不需要的字段,减少无关信息干扰,也能节省成本

MCP Server存在的风险

  • Tool的描述里可以实现提示词注入
  • 使用第三方MCP Server时最好能Review下源代码,配置时固定版本号