# Directory Structure
```
├── .gitignore
├── .python-version
├── .vscode
│ └── settings.json
├── LICENSE
├── mcp-server-diff-python.code-workspace
├── pyproject.toml
├── README.md
├── src
│ └── mcp_server_diff_python
│ ├── __init__.py
│ └── server.py
├── tests
│ └── mcp_server_diff_python
│ └── test_server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
3.11
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
.coverage
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# mcp-server-diff-python
An MCP server for obtaining text differences between two strings.
This server leverages Python's standard library `difflib` to efficiently generate and provide differences between two texts in Unified diff format, making it ideal for text comparison and version control purposes.
<a href="https://glama.ai/mcp/servers/qbwsx2g4vd"><img width="380" height="200" src="https://glama.ai/mcp/servers/qbwsx2g4vd/badge" alt="Server Diff Python MCP server" /></a>
## Features
### Tools
The server provides a single tool:
- **get-unified-diff**: Get differences between two texts in Unified diff format
- Arguments:
- `string_a`: Source text for comparison (required)
- `string_b`: Target text to compare against (required)
- Return value: A string containing the differences in Unified diff format
## Usage
### Claude Desktop
Using with Claude Desktop
To use with Claude Desktop, add the server config:
On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
```json
"mcpServers": {
"mcp-server-diff-python": {
"command": "uvx",
"args": [
"mcp-server-diff-python"
]
}
}
```
or Add the following configuration:
```bash
git clone https://github.com/tatn/mcp-server-diff-python.git
cd mcp-server-diff-python
uv sync
uv build
```
```json
"mcpServers": {
"mcp-server-diff-python": {
"command": "uv",
"args": [
"--directory",
"path\\to\\mcp-server-diff-python",
"run",
"mcp-server-diff-python"
]
}
}
```
## Development
### Debugging
You can start the MCP Inspector using [npx](https://docs.npmjs.com/cli/v11/commands/npx)with the following commands:
```bash
npx @modelcontextprotocol/inspector uvx mcp-server-diff-python
```
```bash
npx @modelcontextprotocol/inspector uv --directory path\to\mcp-server-diff-python run mcp-server-diff-python
```
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
```
--------------------------------------------------------------------------------
/src/mcp_server_diff_python/__init__.py:
--------------------------------------------------------------------------------
```python
import asyncio
from . import server
def main():
"""Main entry point for the package."""
asyncio.run(server.main())
# Optionally expose other important items at package level
__all__ = ["main", "server"]
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "mcp-server-diff-python"
version = "0.1.2"
description = "This is an MCP server that gets the difference between two texts."
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
]
dependencies = [ "mcp>=1.2.0",]
[[project.authors]]
name = "tatn"
[build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"black>=24.10.0",
"build>=1.2.2.post1",
"hatch>=1.14.0",
"mypy>=1.14.1",
"pytest>=8.3.4",
"pytest-asyncio>=0.25.2",
"pytest-cov>=6.0.0",
"ruff>=0.9.2",
"twine>=6.0.1",
]
[project.urls]
Homepage = "https://github.com/tatn/mcp-server-diff-python"
Repository = "https://github.com/tatn/mcp-server-diff-python"
[project.scripts]
mcp-server-diff-python = "mcp_server_diff_python:main"
[tool.pytest.ini_options]
addopts = "-ra -q"
testpaths = ["tests"]
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_server_diff_python"]
[tool.black]
line-length = 88
target-version = ['py311']
include = '\.pyi?$'
[tool.ruff]
lint.select = ["E", "F", "I", "N"]
line-length = 88
target-version = "py311"
[tool.mypy]
python_version = "3.11"
strict = true
```
--------------------------------------------------------------------------------
/src/mcp_server_diff_python/server.py:
--------------------------------------------------------------------------------
```python
import difflib
import mcp.server.stdio
import mcp.types as types
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
server = Server("mcp-server-diff-python")
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
List available tools.
Each tool specifies its arguments using JSON Schema validation.
"""
return [
types.Tool(
name="get-unified-diff",
description="Get the difference between two text articles in Unified diff format. Use this when you want to extract the difference between texts.", # noqa: E501
inputSchema={
"type": "object",
"properties": {
"string_a": {"type": "string"},
"string_b": {"type": "string"},
},
"required": ["string_a", "string_b"],
},
)
]
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
Handle tool execution requests.
Tools can modify server state and notify clients of changes.
"""
if name != "get-unified-diff":
raise ValueError(f"Unknown tool: {name}")
if not arguments:
raise ValueError("Missing arguments")
string_a: str = arguments.get("string_a")
string_b: str = arguments.get("string_b")
if string_a is None or string_b is None:
raise ValueError("Missing 'string_a' or 'string_b' in arguments")
diff_iterator = difflib.unified_diff(string_a.splitlines(), string_b.splitlines())
return [types.TextContent(type="text", text="\n".join(diff_iterator))]
async def main():
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="mcp-server-diff-python",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
```
--------------------------------------------------------------------------------
/tests/mcp_server_diff_python/test_server.py:
--------------------------------------------------------------------------------
```python
import mcp.types as types
import pytest
from mcp_server_diff_python.server import handle_call_tool, handle_list_tools
@pytest.mark.asyncio
async def test_handle_list_tools():
tools = await handle_list_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.name == "get-unified-diff"
assert (
tool.description
== "Get the difference between two text articles in Unified diff format. Use this when you want to extract the difference between texts." # noqa: E501
)
assert "string_a" in tool.inputSchema["properties"]
assert "string_b" in tool.inputSchema["properties"]
assert "string_a" in tool.inputSchema["required"]
assert "string_b" in tool.inputSchema["required"]
@pytest.mark.asyncio
async def test_handle_call_tool():
arguments = {
"string_a": "line1\nline2\nline3",
"string_b": "line1\nline2 modified\nline3",
}
result = await handle_call_tool("get-unified-diff", arguments)
assert len(result) == 1
assert isinstance(result[0], types.TextContent)
assert result[0].type == "text"
excepted = (
"--- \n\n+++ \n\n@@ -1,3 +1,3 @@\n\n line1\n-line2\n+line2 modified\n line3"
)
assert excepted in result[0].text
@pytest.mark.asyncio
async def test_handle_call_tool_missing_arguments():
with pytest.raises(ValueError, match="Missing arguments"):
await handle_call_tool("get-unified-diff", None)
@pytest.mark.asyncio
async def test_handle_call_tool_unknown_tool():
with pytest.raises(ValueError, match="Unknown tool: unknown-tool"):
await handle_call_tool("unknown-tool", {"string_a": "a", "string_b": "b"})
@pytest.mark.asyncio
async def test_handle_call_tool_missing_string_a():
with pytest.raises(
ValueError, match="Missing 'string_a' or 'string_b' in arguments"
):
await handle_call_tool("get-unified-diff", {"string_b": "b"})
@pytest.mark.asyncio
async def test_handle_call_tool_missing_string_b():
with pytest.raises(
ValueError, match="Missing 'string_a' or 'string_b' in arguments"
):
await handle_call_tool("get-unified-diff", {"string_a": "a"})
@pytest.mark.asyncio
async def test_handle_call_tool_empty_strings():
arguments = {"string_a": "", "string_b": ""}
result = await handle_call_tool("get-unified-diff", arguments)
assert len(result) == 1
assert isinstance(result[0], types.TextContent)
assert result[0].type == "text"
assert result[0].text == ""
@pytest.mark.asyncio
async def test_handle_call_tool_identical_strings():
arguments = {
"string_a": "line1\nline2\nline3",
"string_b": "line1\nline2\nline3",
}
result = await handle_call_tool("get-unified-diff", arguments)
assert len(result) == 1
assert isinstance(result[0], types.TextContent)
assert result[0].type == "text"
assert result[0].text == ""
@pytest.mark.asyncio
async def test_handle_call_tool_different_strings():
arguments = {
"string_a": "line1\nline2\nline3",
"string_b": "line1\nline2\nline4",
}
result = await handle_call_tool("get-unified-diff", arguments)
assert len(result) == 1
assert isinstance(result[0], types.TextContent)
assert result[0].type == "text"
assert (
"--- \n+++ \n@@ -1,3 +1,3 @@\n line1\n line2\n-line3\n+line4"
in result[0].text
)
```