Skip to content

Commit 328f0e6

Browse files
committed
💩(mcp) add a local MCP server configuration
This provides a way to start a local MCP server: - provided a user token, the MCP can create document - can be run locally and work with cursor or mcphost
1 parent b73c31b commit 328f0e6

File tree

15 files changed

+978
-0
lines changed

15 files changed

+978
-0
lines changed

src/mcp_server/.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.venv
2+
.env
3+
docker-compose.yaml
4+
Dockerfile

src/mcp_server/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

src/mcp_server/Dockerfile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
FROM python:3.12-slim
2+
3+
USER root
4+
5+
# Install uv.
6+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
7+
8+
# Change the working directory to the `app` directory
9+
WORKDIR /app
10+
11+
# Install dependencies
12+
RUN --mount=type=cache,target=/root/.cache/uv \
13+
--mount=type=bind,source=uv.lock,target=uv.lock \
14+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
15+
uv sync --locked --no-install-project
16+
17+
# Copy the application into the image
18+
COPY docs_mcp_server/ /app/docs_mcp_server/
19+
20+
# Sync the project
21+
RUN --mount=type=cache,target=/root/.cache/uv \
22+
--mount=type=bind,source=uv.lock,target=uv.lock \
23+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
24+
uv sync --locked
25+
26+
# Attach ports
27+
EXPOSE 4200
28+
ENV SERVER_HOST=0.0.0.0
29+
30+
# Un-privileged user running the application
31+
ARG DOCKER_USER
32+
USER ${DOCKER_USER}
33+
34+
# Run the MCP server.
35+
CMD ["uv", "--no-cache", "run", "python", "-m" ,"docs_mcp_server.mcp_server"]

src/mcp_server/Makefile

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
2+
#
3+
# This Makefile is only meant to be used for DEVELOPMENT purpose as we are
4+
# changing the user id that will run in the container.
5+
#
6+
# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER...
7+
#
8+
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
9+
#
10+
# Note to developers:
11+
#
12+
# While editing this file, please respect the following statements:
13+
#
14+
# 1. Every variable should be defined in the ad hoc VARIABLES section with a
15+
# relevant subsection
16+
# 2. Every new rule should be defined in the ad hoc RULES section with a
17+
# relevant subsection depending on the targeted service
18+
# 3. Rules should be sorted alphabetically within their section
19+
# 4. When a rule has multiple dependencies, you should:
20+
# - duplicate the rule name to add the help string (if required)
21+
# - write one dependency per line to increase readability and diffs
22+
# 5. .PHONY rule statement should be written after the corresponding rule
23+
# ==============================================================================
24+
# VARIABLES
25+
26+
27+
28+
BOLD := \033[1m
29+
RESET := \033[0m
30+
GREEN := \033[1;32m
31+
32+
# Use uv for package management
33+
UV = uv
34+
35+
# ==============================================================================
36+
# RULES
37+
38+
default: help
39+
40+
help: ## Display this help message
41+
@echo "$(BOLD)Docs MCP server Makefile"
42+
@echo "Please use 'make $(BOLD)target$(RESET)' where $(BOLD)target$(RESET) is one of:"
43+
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-30s$(RESET) %s\n", $$1, $$2}'
44+
.PHONY: help
45+
46+
install: ## Install the project
47+
@$(UV) sync
48+
.PHONY: install
49+
50+
install-dev: ## Install the project with dev dependencies
51+
@$(UV) sync --extra dev
52+
.PHONY: install-dev
53+
54+
clean: ## Clean the project folder
55+
@rm -rf build/
56+
@rm -rf dist/
57+
@rm -rf *.egg-info
58+
@find . -type d -name __pycache__ -exec rm -rf {} +
59+
@find . -type f -name "*.pyc" -delete
60+
.PHONY: clean
61+
62+
format: ## Run the formatter
63+
@$(UV) run ruff format
64+
.PHONY: format
65+
66+
lint: format ## Run the linter
67+
@$(UV) run ruff check --fix .
68+
.PHONY: lint
69+
70+
test: ## Run the tests
71+
@cd tests && PYTHON_PATH=.:$(PYTHON_PATH) $(UV) run python -m pytest . -vvv
72+
.PHONY: test
73+
74+
runserver: ## Run the project server
75+
@$(UV) run python -m docs_mcp_server.mcp_server
76+
.PHONY: runserver
77+
78+
runserver-docker: ## Run the project server in a docker container
79+
@touch .env
80+
@docker compose up --watch
81+
.PHONY: runserver-docker
82+
83+
run_llm: ## Run the LLM server
84+
@mcphost -m ollama:qwen2.5:3b --config "$(CWD)/mcphost.json"

src/mcp_server/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# mcp_server
2+
3+
`mcp_server` is a backend server application designed to manage and process MCP requests.
4+
5+
## Features
6+
7+
- Create a new Docs with a title and markdown content from a request to an agentic LLM.
8+
9+
10+
## Configuration
11+
12+
Configuration options can be set via environment variables or a configuration file (`.env`).
13+
14+
Common options include:
15+
16+
- `SERVER_TRANSPORT`: `STDIO`, `SSE` or `STREAMABLE_HTTP` (default: `STDIO` locally or `STREAMABLE_HTTP` in the docker image)
17+
- `SERVER_PATH`: The base path of the server tools and resources (default: `/mcp/docs/`)
18+
19+
You will need to set the following options to allow the MCP server to query Docs API:
20+
21+
- `DOCS_API_URL`: The Docs base URL without the "/api/v1.0/" (default: `http://localhost:8071`)
22+
- `DOCS_API_TOKEN`: The API user token you generate from the Docs frontend
23+
24+
You may customize the following options for local development, while it's not recommended and may break the Docker image:
25+
26+
- `SERVER_HOST`: (default: `localhost` locally and `0.0.0.0` in the docker Image)
27+
- `SERVER_PORT`: (default: `4200`)
28+
29+
Example when using the server from a Docker instance
30+
31+
```dotenv
32+
SERVER_TRANSPORT=SSE
33+
34+
DOCS_API_URL=http://host.docker.internal:8071/
35+
DOCS_API_TOKEN=<some_token>
36+
```
37+
38+
## Run the MCP server
39+
40+
### Local
41+
You may work on the MCP server project using local configuration with `uv`:
42+
43+
```shell
44+
cd src/mcp_server
45+
46+
make install
47+
make runserver
48+
```
49+
50+
### Docker
51+
If you don't have local installation of Python or `uv` you can work using the Docker image:
52+
53+
```shell
54+
cd src/mcp_server
55+
56+
make runserver-docker
57+
```
58+
59+
## Usage
60+
61+
1. Create a local configuration file `.env`
62+
63+
```dotenv
64+
SERVER_TRANSPORT=SSE
65+
66+
DOCS_API_URL=http://host.docker.internal:8071/
67+
DOCS_API_TOKEN=your-token-here
68+
```
69+
70+
2. Run the server
71+
72+
```shell
73+
make runserver-docker
74+
```
75+
76+
### In Cursor IDE
77+
78+
In Cursor settings, in the MCP section, you can add a new MCP server with the following configuration:
79+
80+
```json
81+
{
82+
"mcpServers": {
83+
"docs": {
84+
"url": "http://127.0.0.1:4200/mcp/docs/"
85+
}
86+
}
87+
}
88+
```
89+
90+
### Locally with `mcphost` and `ollama`
91+
92+
1. Install [mcphost](https://github.com/mark3labs/mcphost)
93+
2. Install [ollama](https://ollama.ai)
94+
3. Start ollama: `ollama serve`
95+
4. Pull an agentic model like Qwen2.5 `ollama pull qwen2.5:3b`
96+
5. Create an MCP configuration file (e.g. `mcphost.json`)
97+
98+
```json
99+
{
100+
"mcpServers": {
101+
"docs": {
102+
"url": "http://127.0.0.1:4200/mcp/docs/"
103+
}
104+
}
105+
}
106+
```
107+
108+
6. Start mcphost
109+
110+
```shell
111+
mcphost -m ollama:qwen2.5:3b --config "$PWD/mcphost.json"
112+
```

src/mcp_server/docker-compose.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
services:
2+
docs_mcp-server:
3+
build:
4+
context: .
5+
args:
6+
DOCKER_USER: ${DOCKER_USER:-1000}
7+
user: ${DOCKER_USER:-1000}
8+
extra_hosts:
9+
- "host.docker.internal:host-gateway"
10+
env_file:
11+
- .env
12+
ports:
13+
- "4200:4200"
14+
15+
develop:
16+
# Create a `watch` configuration to update the app
17+
watch:
18+
# Sync the working directory with the `/app` directory in the container
19+
- action: sync+restart
20+
path: ./docs_mcp_server
21+
target: /app/docs_mcp_server/
22+
# Exclude the project virtual environment
23+
ignore:
24+
- .venv/
25+
26+
# Rebuild the image on changes to the `pyproject.toml`
27+
- action: rebuild
28+
path: ./pyproject.toml
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""MCP Server package."""
2+
3+
__version__ = "0.1.0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Authentication module for the MCP server."""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Authentication against Docs API via user token."""
2+
3+
import httpx
4+
5+
6+
class UserTokenAuthentication(httpx.Auth):
7+
"""Authentication class for request made to Docs, using the user token."""
8+
9+
def __init__(self, token):
10+
"""Initialize the authentication class with the user token."""
11+
self.token = token
12+
13+
def auth_flow(self, request):
14+
"""Add the Authorization header to the request with the user token."""
15+
request.headers["Authorization"] = f"Token {self.token}"
16+
yield request
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Project constants."""
2+
3+
import enum
4+
5+
6+
class TransportLayerEnum(enum.Enum):
7+
"""Enum for the MCP server transport layer types."""
8+
9+
STDIO = "stdio"
10+
SSE = "sse"
11+
STREAMABLE_HTTP = "streamable-http"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""The core of the MCP server for the Docs API."""
2+
3+
import asyncio
4+
5+
import httpx
6+
from fastmcp import FastMCP
7+
8+
from . import settings, utils
9+
from .auth.token import UserTokenAuthentication
10+
11+
# Create a server instance from the OpenAPI spec
12+
mcp = FastMCP(name="Docs MCP Server")
13+
14+
15+
def setup_mcp_server():
16+
"""Configure the tools and resources for the MCP server."""
17+
# Create a client for your API
18+
api_client = httpx.AsyncClient(
19+
base_url=settings.DOCS_API_URL,
20+
auth=UserTokenAuthentication(token=settings.DOCS_API_TOKEN),
21+
)
22+
23+
@mcp.tool()
24+
async def create_document_tool(document_title: str, document_content: str) -> None:
25+
"""
26+
Create a new document with the provided title and content.
27+
28+
Args:
29+
document_title: The title of the document (required)
30+
document_content: The content of the document (required)
31+
32+
"""
33+
# Get current user information
34+
user_response = await api_client.get("/api/v1.0/users/me/")
35+
user_response.raise_for_status()
36+
user_data = user_response.json()
37+
38+
# Prepare document data
39+
data = {
40+
"title": document_title,
41+
"content": document_content,
42+
"sub": user_data["id"],
43+
"email": user_data["email"],
44+
}
45+
46+
# Create the document
47+
create_response = await api_client.post(
48+
"/api/v1.0/documents/create-for-owner/",
49+
json=data,
50+
)
51+
create_response.raise_for_status()
52+
53+
54+
if __name__ == "__main__":
55+
setup_mcp_server()
56+
asyncio.run(utils.check_mcp(mcp))
57+
mcp.run(
58+
transport=settings.SERVER_TRANSPORT.value,
59+
host=settings.SERVER_HOST,
60+
port=settings.SERVER_PORT,
61+
path=settings.SERVER_PATH,
62+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Settings for the MCP server."""
2+
3+
import os
4+
5+
from dotenv import load_dotenv
6+
7+
from .constants import TransportLayerEnum
8+
9+
load_dotenv()
10+
11+
12+
# Server settings
13+
SERVER_TRANSPORT = TransportLayerEnum[os.getenv("SERVER_TRANSPORT", "STDIO")]
14+
SERVER_HOST = str(os.getenv("SERVER_HOST", "localhost"))
15+
SERVER_PORT = int(os.getenv("SERVER_PORT", "4200"))
16+
SERVER_PATH = str(os.getenv("SERVER_PATH", "/mcp/docs"))
17+
18+
19+
# Docs related settings
20+
DOCS_API_URL = str(os.getenv("DOCS_API_URL", "http://localhost:8071"))
21+
DOCS_API_TOKEN = str(os.getenv("DOCS_API_TOKEN", "")) or None

0 commit comments

Comments
 (0)