Skip to content

Commit 0f32206

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 0f32206

File tree

16 files changed

+1022
-0
lines changed

16 files changed

+1022
-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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 if you want
23+
to use token based authentication (default: `None`). If not provided, the server will
24+
use the authentication forwarder (pass the incoming authentication header to the Docs API call).
25+
26+
You may customize the following options for local development, while it's not recommended and may break the Docker image:
27+
28+
- `SERVER_HOST`: (default: `localhost` locally and `0.0.0.0` in the docker Image)
29+
- `SERVER_PORT`: (default: `4200`)
30+
31+
Example when using the server from a Docker instance
32+
33+
```dotenv
34+
SERVER_TRANSPORT=SSE
35+
36+
DOCS_API_URL=http://host.docker.internal:8071/
37+
DOCS_API_TOKEN=<some_token>
38+
```
39+
40+
## Run the MCP server
41+
42+
### Local
43+
You may work on the MCP server project using local configuration with `uv`:
44+
45+
```shell
46+
cd src/mcp_server
47+
48+
make install
49+
make runserver
50+
```
51+
52+
### Docker
53+
If you don't have local installation of Python or `uv` you can work using the Docker image:
54+
55+
```shell
56+
cd src/mcp_server
57+
58+
make runserver-docker
59+
```
60+
61+
## Usage
62+
63+
1. Create a local configuration file `.env`
64+
65+
```dotenv
66+
SERVER_TRANSPORT=SSE
67+
68+
DOCS_API_URL=http://host.docker.internal:8071/
69+
DOCS_API_TOKEN=your-token-here
70+
```
71+
72+
2. Run the server
73+
74+
```shell
75+
make runserver-docker
76+
```
77+
78+
### In Cursor IDE
79+
80+
In Cursor settings, in the MCP section, you can add a new MCP server with the following configuration:
81+
82+
```json
83+
{
84+
"mcpServers": {
85+
"docs": {
86+
"url": "http://127.0.0.1:4200/mcp/docs/"
87+
}
88+
}
89+
}
90+
```
91+
92+
### Locally with `mcphost` and `ollama`
93+
94+
1. Install [mcphost](https://github.com/mark3labs/mcphost)
95+
2. Install [ollama](https://ollama.ai)
96+
3. Start ollama: `ollama serve`
97+
4. Pull an agentic model like Qwen2.5 `ollama pull qwen2.5:3b`
98+
5. Create an MCP configuration file (e.g. `mcphost.json`)
99+
100+
```json
101+
{
102+
"mcpServers": {
103+
"docs": {
104+
"url": "http://127.0.0.1:4200/mcp/docs/"
105+
}
106+
}
107+
}
108+
```
109+
110+
6. Start mcphost
111+
112+
```shell
113+
mcphost -m ollama:qwen2.5:3b --config "$PWD/mcphost.json"
114+
```
115+
116+
117+
## About the authentication forwarder
118+
119+
The authentication forwarder is a simple proxy that forwards the authentication header from
120+
the incoming request to the Docs API call. This allows to use "resource server" authentication.
121+
122+
For instance:
123+
124+
- Docs authentication is based on OIDC with Keycloak.
125+
- The AI chat is using the same Keycloak instance for authentication.
126+
- You can store the access token in the chat session and use it when calling the MCP server.
127+
- The MCP server will forward the access token to the Docs API call
128+
(actually, it forwards the whole authentication header).
129+
- Docs will introspect the access token and authenticate the user.
130+
- Conclusion: the user will be able to create a new Doc with the same access token
131+
used in the chat session.

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Authentication against Docs API via user token."""
2+
3+
import httpx
4+
from fastmcp.server.dependencies import get_http_request
5+
6+
7+
class HeaderForwarderAuthentication(httpx.Auth):
8+
"""Authentication class for request made to Docs, work as boilerplate."""
9+
10+
def auth_flow(self, request):
11+
"""Get Authorization header from request and pass it to the client."""
12+
_incoming_request = get_http_request()
13+
14+
# Get authorization header
15+
auth_header = _incoming_request.headers.get("authorization", "")
16+
17+
request.headers["Authorization"] = auth_header
18+
yield request
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

0 commit comments

Comments
 (0)