ContextServer is a local Python MCP server for repository inspection. It gives MCP-compatible AI clients a small, read-only toolset for reading files, listing directories, searching text, searching documentation, and inspecting Git state inside a configured repository root.
It is best described as:
Python backend process
-> MCP tool server
-> read-only repository context provider for AI agents
It is not a REST API, browser extension, hosted SaaS backend, or plugin marketplace package. MCP clients communicate with it through the Model Context Protocol over stdio. In practice, an MCP client starts ContextServer as a subprocess and sends tools/list and tools/call requests over standard input/output. The project also includes a local browser dashboard for manually managing stdio MCP server definitions and calling their tools.
Implemented:
- MCP stdio server using the official
mcpPython SDK - Tool discovery through
tools/list - Tool invocation through
tools/call - JSON Schema validation for tool parameters
- 9 read-only tools:
read_filelist_directorysearch_filesgit_loggit_diffgit_statusgit_branchesgrep_searchdocs_search
- Internal plugin-style tool registration for filesystem, Git, search, and documentation tool groups
- Interactive CLI client for manually testing tools
- Local web dashboard for listing/calling tools and adding other stdio MCP servers
- Path containment checks for filesystem access
- Test suite covering path safety, filesystem tools, Git tools, search tools, documentation search, plugin registration, MCP stdio integration, web UI helpers, registry behavior, and CLI argument parsing
Current scope:
- Local stdio MCP server, designed to be launched by an MCP-compatible client.
- Read-only repository inspection, not file editing or code modification.
- Literal search and documentation search, not embedding-based semantic retrieval.
- Developer-focused CLI and local browser dashboard for manual testing, not a hosted service.
The CLI accepts --transport stdio because stdio is the supported transport for this local MCP server.
MCP Client / Interactive CLI
|
| stdio transport
v
ContextServer
|
v
ToolRegistry
|
+-- Filesystem tools
| +-- read_file
| +-- list_directory
| +-- search_files
|
+-- Git tools
| +-- git_log
| +-- git_diff
| +-- git_status
| +-- git_branches
|
+-- Search tools
| +-- grep_search
|
+-- Documentation tools
+-- docs_search
The server process is configured with one repository root. All filesystem paths are resolved relative to that root and validated before file I/O. Git commands run with the repository root as the subprocess working directory. Built-in tools are grouped into internal plugin modules, so new tool groups can be registered without changing MCP request routing.
ContextServer/
├── client/
│ ├── __init__.py
│ └── cli.py
├── data/
│ └── docs/
│ ├── .gitkeep
│ └── contextserver.md
├── server/
│ ├── __init__.py
│ ├── main.py
│ ├── mcp_server.py
│ ├── path_utils.py
│ ├── tool_registry.py
│ └── web_ui.py
├── tests/
│ ├── __init__.py
│ ├── test_docs_tools.py
│ ├── test_filesystem_tools.py
│ ├── test_git_tools.py
│ ├── test_main.py
│ ├── test_mcp_stdio_integration.py
│ ├── test_path_utils.py
│ ├── test_search_tools.py
│ ├── test_tool_plugins.py
│ ├── test_tool_registry.py
│ └── test_web_ui.py
├── tool_plugins/
│ ├── __init__.py
│ ├── docs.py
│ ├── filesystem.py
│ ├── git.py
│ └── search.py
├── tools/
│ ├── __init__.py
│ ├── docs_tools.py
│ ├── filesystem_tools.py
│ ├── git_tools.py
│ └── search_tools.py
├── assets/
│ └── contextserver-architecture.png
├── .gitignore
├── LICENSE
├── README.md
└── pyproject.toml
| File | Purpose |
|---|---|
server/main.py |
CLI entry point. Parses arguments, validates the repository root, registers tools, and starts the MCP server. |
server/mcp_server.py |
Wraps the MCP SDK low-level server and exposes tools/list and tools/call. |
server/tool_registry.py |
Stores tool definitions, returns tool manifests, validates parameters, and dispatches calls. |
server/path_utils.py |
Resolves paths and enforces containment inside the configured repository root. |
server/web_ui.py |
Local browser dashboard for managing stdio MCP server definitions and calling tools. |
tool_plugins/ |
Internal plugin-style registration modules for built-in tool groups. |
tools/docs_tools.py |
Implements docs_search. |
tools/filesystem_tools.py |
Implements read_file, list_directory, and search_files. |
tools/git_tools.py |
Implements git_log, git_diff, git_status, and git_branches. |
tools/search_tools.py |
Implements grep_search. |
client/cli.py |
Starts ContextServer over stdio and provides a small REPL for manual testing. |
tests/ |
Automated test suite. |
- Python 3.10 or newer
- Git CLI on
PATHfor Git tools - A Git repository if you want to use
git_*tools
Runtime Python dependencies:
| Package | Purpose |
|---|---|
mcp |
MCP server/client SDK |
jsonschema |
Tool input validation |
Development dependencies:
| Package | Purpose |
|---|---|
pytest |
Test runner |
pytest-asyncio |
Async test support |
hypothesis |
Property-based testing support |
From the repository root:
pip install -e .For development:
pip install -e ".[dev]"If your default Python is older than 3.10, create a compatible virtual environment first:
python3.10 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"Serve the current directory:
python -m server.mainServe a specific repository:
python -m server.main --repo-root /path/to/repoUse the installed console script:
context-server --repo-root /path/to/repoExplicitly specify stdio transport:
python -m server.main --repo-root /path/to/repo --transport stdioExpected startup message:
Starting ContextServer with transport: stdio
After startup, the process waits for MCP messages on stdio. Running it directly in a terminal is useful only as a startup smoke test. Press Ctrl+C to stop it.
ContextServer includes a local browser dashboard for manually working with MCP servers. The dashboard can:
- Show the built-in ContextServer entry for the selected repository root.
- List tools from any configured stdio MCP server.
- Call tools with JSON arguments.
- Add and remove user-defined stdio MCP server definitions.
Start the dashboard:
python -m server.web_ui --repo-root /path/to/repoOr use the installed console script:
context-server-ui --repo-root /path/to/repoExpected startup output:
ContextServer UI: http://127.0.0.1:8765
Repository root: /path/to/repo
MCP server config: /path/to/ContextServer/.contextserver/mcp_servers.json
Open the shown URL in a browser.
The dashboard supports stdio MCP servers. To add one, provide:
| Field | Description |
|---|---|
| Name | Display name in the dashboard. |
| Command | Executable command, such as python, node, or an installed MCP server binary. |
| Args | Shell-style argument string, such as -m package.server --flag value. |
| Working directory | Optional process working directory. |
User-added server definitions are stored locally in .contextserver/mcp_servers.json, which is ignored by Git. Commands run on your machine, so only add MCP servers you trust.
The included client starts ContextServer as a subprocess over stdio and gives you a REPL for manual tool testing.
python -m client.cli --repo-root /path/to/repoExample session:
Connecting to ContextServer (repo: /path/to/repo) ...
Connected. Type 'help' for available commands.
mcp> list_tools
mcp> call read_file {"path": "README.md", "start_line": 1, "end_line": 5}
mcp> call list_directory {"path": ".", "depth": 1}
mcp> call search_files {"pattern": "**/*.py"}
mcp> call grep_search {"query": "ContextServer", "include_pattern": "*.md"}
mcp> call docs_search {"query": "ContextServer"}
mcp> call git_status {}
mcp> quit
Disconnected.
CLI commands:
| Command | Description |
|---|---|
list_tools |
List all tools exposed by the server. |
call <name> <json> |
Call a tool with JSON parameters. |
help |
Show REPL help. |
quit / exit |
Exit the REPL. |
ContextServer can be configured in an MCP-compatible client by pointing the client at the Python module entry point.
Example mcp.json:
{
"mcpServers": {
"ContextServer": {
"command": "python",
"args": [
"-m",
"server.main",
"--repo-root",
"/path/to/repo"
],
"cwd": "/path/to/ContextServer"
}
}
}If installed with pip install -e ., you can also use the console script:
{
"mcpServers": {
"ContextServer": {
"command": "context-server",
"args": [
"--repo-root",
"/path/to/repo"
]
}
}
}The exact config file location and schema depend on the MCP client you are using.
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
async def main() -> None:
params = StdioServerParameters(
command="python",
args=[
"-m",
"server.main",
"--repo-root",
"/path/to/repo",
],
cwd="/path/to/ContextServer",
)
async with stdio_client(params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools = await session.list_tools()
print([tool.name for tool in tools.tools])
result = await session.call_tool(
"read_file",
{"path": "README.md", "start_line": 1, "end_line": 5},
)
print(result.content[0].text)
asyncio.run(main())All tool parameters are JSON objects. Tool outputs are returned as MCP TextContent. String results are returned directly. Non-string results are JSON-serialized before being placed in TextContent.
Known tool errors are returned as MCP error responses with isError: true; they do not crash the server.
Read UTF-8 text content from a file inside the configured repository root.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
path |
string | Yes | File path relative to the repository root. Absolute paths are accepted only if they resolve inside the repository root. |
start_line |
integer | No | 1-indexed first line to include. |
end_line |
integer | No | 1-indexed final line to include, inclusive. |
Examples:
{"path": "README.md"}{"path": "server/main.py", "start_line": 1, "end_line": 20}Returns:
file contents
Behavior:
- Reads the full file when no line range is supplied.
- Uses 1-indexed line numbers.
- Treats
end_lineas inclusive. - Opens files as UTF-8 text.
- Raises
FileNotFoundErrorif the resolved path is not a file. - Raises
PathContainmentErrorif the path resolves outside the repository root.
List files and directories inside a directory.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
path |
string | Yes | Directory path relative to the repository root. |
depth |
integer | No | Recursion depth. Defaults to 1. A depth of 1 returns immediate children only. |
Example:
{"path": ".", "depth": 2}Returns:
[
{"name": "README.md", "type": "file"},
{"name": "server", "type": "directory"},
{"name": "server/main.py", "type": "file"}
]Behavior:
- Returns a flat list.
- Sorts child names before collecting entries.
- Uses
typevalues offileordirectory. - Returns nested entry names relative to the requested directory.
- Silently skips unreadable subdirectories.
- Raises
FileNotFoundErrorif the resolved path is not a directory. - Raises
PathContainmentErrorif the path resolves outside the repository root.
Find files by glob pattern.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
pattern |
string | Yes | Glob pattern such as *.py, **/*.py, or *.json. |
base_dir |
string | No | Subdirectory to search from. Defaults to the repository root. |
Examples:
{"pattern": "**/*.py"}{"pattern": "*.json", "base_dir": "config"}Returns:
[
"client/cli.py",
"server/main.py",
"tools/git_tools.py"
]Behavior:
- Uses
pathlib.Path.glob. - Returns files only, not directories.
- Returns paths relative to the repository root.
- Sorts results before returning.
- Raises
PathContainmentErrorifbase_dirresolves outside the repository root.
Return recent commits from the configured repository.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
max_count |
integer | No | Maximum number of commits to return. Defaults to 10. |
file_path |
string | No | Restrict history to commits that touched this file. |
Examples:
{}{"max_count": 5}{"file_path": "README.md", "max_count": 3}Returns:
[
{
"hash": "360cb49...",
"author": "Srujay Reddy",
"date": "2026-06-01T12:00:00-07:00",
"message": "Add ContextServer implementation"
}
]Behavior:
- Runs
git log. - Returns full commit hashes.
- Uses ISO author dates from Git.
- Returns an empty list for Git errors such as an empty repository.
- Raises
GitNotAvailableErrorifgitis missing or no.gitdirectory exists at the configured repository root.
Return Git diff text.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
ref1 |
string | No | First Git reference. |
ref2 |
string | No | Second Git reference. |
Examples:
{}{"ref1": "HEAD~1"}{"ref1": "HEAD~1", "ref2": "HEAD"}Behavior:
- With no refs, runs
git diffand returns uncommitted working-tree changes. - With
ref1, returns the diff betweenref1and the working tree. - With
ref1andref2, returns the diff between those refs. - Verifies supplied refs with
git rev-parse --verify. - Returns an empty string when there is no diff.
- Raises
GitRefNotFoundErrorfor invalid refs. - Raises
GitNotAvailableErrorifgitis missing or no.gitdirectory exists at the configured repository root.
Return branch and file status.
Parameters:
{}Returns:
{
"branch": "main",
"modified": ["README.md"],
"staged": ["server/main.py"],
"untracked": ["notes.txt"]
}Behavior:
- Runs
git status --porcelain=v1 --branch. - Parses the current branch name.
- Separates modified, staged, and untracked paths.
- Handles renamed paths by reporting the new path.
- Raises
GitNotAvailableErrorifgitis missing, no.gitdirectory exists, orgit statusfails.
Return local Git branches.
Parameters:
{}Returns:
[
{"name": "main", "is_current": true},
{"name": "feature-x", "is_current": false}
]Behavior:
- Runs
git branch --list. - Returns local branches only.
- Marks the checked-out branch with
is_current: true. - Skips detached HEAD entries.
- Raises
GitNotAvailableErrorifgitis missing, no.gitdirectory exists, orgit branchfails.
Search UTF-8 text files line by line for a literal query.
This is a plain full-text search tool. It does not perform semantic search, ranking, fuzzy matching, or documentation indexing.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | Literal text to search for. The query is escaped before regex compilation. |
include_pattern |
string | No | Glob-style filter matched against repository-relative paths. |
case_sensitive |
boolean | No | Whether matching is case-sensitive. Defaults to true. |
Examples:
{"query": "TODO"}{"query": "def main", "include_pattern": "*.py"}{"query": "contextserver", "case_sensitive": false}Returns:
[
{
"file": "server/main.py",
"line_number": 37,
"content": " description=\"ContextServer - MCP tool server for repository inspection\",",
"context_before": [
"def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:",
" \"\"\"Parse command-line arguments.\"\"\""
],
"context_after": [
" )",
" parser.add_argument("
]
}
]Behavior:
- Walks the configured repository root with
os.walk. - Reads candidate files as UTF-8 text.
- Skips binary, unreadable, or non-UTF-8 files.
- Returns every matching line.
- Includes up to 2 lines of context before and after each match.
- Uses literal matching by escaping the supplied query with
re.escape. - Uses
fnmatch.fnmatchforinclude_pattern.
Search documentation-like UTF-8 text files line by line for a literal query.
This is a documentation-focused grep tool. It searches .md, .mdx, .txt, and .rst files only. It does not perform semantic search, embeddings, ranking, or fuzzy matching.
Default search root:
docs/when present.data/docs/whendocs/is absent anddata/docs/is present.- Repository root when neither docs directory exists, filtered to documentation-like file extensions.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | Literal text to search for. The query is escaped before regex compilation. |
docs_dir |
string | No | Documentation directory to search, relative to the repository root. |
case_sensitive |
boolean | No | Whether matching is case-sensitive. Defaults to false. |
max_results |
integer | No | Maximum number of matches to return. Defaults to 50. Minimum 1. |
Examples:
{"query": "ContextServer"}{"query": "install", "docs_dir": "docs"}{"query": "MCP", "case_sensitive": true, "max_results": 10}Returns:
[
{
"file": "data/docs/contextserver.md",
"line_number": 3,
"content": "ContextServer is an MCP tool server that lets AI agents inspect repositories",
"context_before": [
"# ContextServer Documentation",
""
],
"context_after": [
"through read-only filesystem tools, Git introspection, and documentation search."
]
}
]Behavior:
- Uses the default docs root unless
docs_diris provided. - Validates
docs_dirwith the same path containment checks as filesystem tools. - Searches only
.md,.mdx,.txt, and.rstfiles. - Skips binary, unreadable, or non-UTF-8 files.
- Returns every matching line until
max_resultsis reached. - Includes up to 2 lines of context before and after each match.
- Uses literal matching by escaping the supplied query with
re.escape. - Is case-insensitive by default.
ContextServer is read-only by design.
Implemented protections:
- No tool creates, edits, or deletes files.
- Filesystem paths pass through
resolve_and_validate. - Paths are resolved with
os.path.realpath. - Symlinks are followed before containment is checked.
../traversal is blocked when it resolves outside the repository root.- Prefix false positives are blocked. For example,
/repo-extrais not treated as inside/repo. - Git subprocesses run with
cwdset to the configured repository root.
Important boundaries:
- This server is intended for local trusted MCP client use.
- It does not implement user authentication.
- It does not sandbox Git itself beyond setting the working directory.
- It does not restrict read access by file extension.
- It skips unreadable or non-UTF-8 files for grep, but
read_fileexpects UTF-8 text.
The MCP server catches known tool-layer exceptions and returns MCP error responses instead of crashing.
Known errors:
| Error | Source | Meaning |
|---|---|---|
ToolNotFoundError |
server/tool_registry.py |
A client requested an unknown tool. |
InvalidParametersError |
server/tool_registry.py |
Tool arguments failed JSON Schema validation. |
PathContainmentError |
server/path_utils.py |
A path resolved outside the repository root. |
FileNotFoundError |
Filesystem tools | A requested file or directory does not exist. |
GitNotAvailableError |
Git tools | Git is unavailable or the configured root is not a Git repository. |
GitRefNotFoundError |
Git tools | A supplied Git ref is invalid. |
Error responses are returned as TextContent with isError: true.
Unexpected exceptions are caught, logged, and returned as internal error responses.
Run all tests:
python -m pytestExpected result:
124 passed
Run a single test module:
python -m pytest tests/test_filesystem_tools.py -v
python -m pytest tests/test_git_tools.py -v
python -m pytest tests/test_search_tools.py -vCheck repository state:
git status --short --branchExpected clean state:
## main...origin/main
Check installed console script:
context-server --helpExpected: argparse help showing --repo-root and --transport.
Smoke test server startup:
python -m server.main --repo-root .Expected:
Starting ContextServer with transport: stdio
Then press Ctrl+C.
Manual tool test:
python -m client.cli --repo-root .Then run:
list_tools
call read_file {"path": "README.md", "start_line": 1, "end_line": 5}
call list_directory {"path": ".", "depth": 1}
call search_files {"pattern": "**/*.py"}
call grep_search {"query": "ContextServer", "include_pattern": "*.md"}
call docs_search {"query": "ContextServer"}
call git_status {}
quit
The current test suite covers:
| Module | Coverage Focus |
|---|---|
tests/test_docs_tools.py |
Documentation search defaults, docs directory override, case sensitivity, max results, path safety. |
tests/test_path_utils.py |
Path resolution, root containment, traversal prevention, symlink handling. |
tests/test_tool_registry.py |
Tool registration, manifest generation, validation, unknown tools, dispatch. |
tests/test_tool_plugins.py |
Internal plugin registration and complete tool manifest coverage. |
tests/test_filesystem_tools.py |
File reading, line ranges, directory listing, glob search, path safety. |
tests/test_git_tools.py |
Git log, diff, status, branches, invalid refs, unavailable Git repositories. |
tests/test_search_tools.py |
Literal grep, context lines, include patterns, case sensitivity, binary skip behavior. |
tests/test_mcp_stdio_integration.py |
End-to-end MCP stdio tools/list and tools/call behavior. |
tests/test_main.py |
Argument parsing and repository root validation. |
tests/test_web_ui.py |
Local web UI config helpers and built-in MCP server tool listing. |
- Only stdio transport is implemented.
- Search is literal grep-style full-text search, not semantic search.
- Documentation search is docs-focused literal search, not semantic documentation indexing.
- Tool outputs are returned as text content. Structured outputs are JSON-serialized into text.
- Filesystem tools are read-only.
- Git tools require the configured repository root to contain
.git. - The CLI client and web dashboard are manual testing helpers, not hosted multi-user applications.
Registering tools happens through internal plugin modules. server/main.py creates a registry and delegates tool registration:
registry = ToolRegistry()
register_all(registry, resolved_root)Each built-in plugin in tool_plugins/ owns one tool group and exposes register(registry, repo_root).
Each tool module exposes:
- one or more async handler functions
- JSON Schema parameter definitions
make_*_tool()factory functions returningToolDefinitionset_repo_root()for module-level repository configuration
The registry validates input with jsonschema.validate before dispatching to the handler.
MIT
