From b79475e957127b2d7f8ba944297b30b8b892783e Mon Sep 17 00:00:00 2001 From: Oswaldo Gomez Date: Sun, 9 Nov 2025 18:16:14 +0100 Subject: [PATCH 1/6] Making the dockerfile and code ready for kagent. --- Dockerfile | 10 ++++++ Makefile | 58 ++++++++++++++++++++++++++++++++++ README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++ src/m3/mcp_server.py | 15 +++++++-- 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 Makefile diff --git a/Dockerfile b/Dockerfile index 99ef59e..ec0ea77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,9 +27,19 @@ RUN m3 init mimic-iv-demo # Lite: SQLite only FROM base AS lite +ENV MCP_TRANSPORT=http \ + MCP_HOST=0.0.0.0 \ + MCP_PORT=3000 \ + MCP_PATH=/sse +EXPOSE 3000 CMD ["python", "-m", "m3.mcp_server"] # BigQuery: add GCP client FROM base AS bigquery RUN pip install --no-cache-dir google-cloud-bigquery +ENV MCP_TRANSPORT=http \ + MCP_HOST=0.0.0.0 \ + MCP_PORT=3000 \ + MCP_PATH=/sse +EXPOSE 3000 CMD ["python", "-m", "m3.mcp_server"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..57b1a76 --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +# Makefile for M3 Docker Image Build and Push +DOCKER ?= docker +IMAGE_NAME := m3-mimic-demo +IMAGE_TAG ?= 0.0.3 + +# Prompt for registry only if not set +ifndef DOCKER_REGISTRY +DOCKER_REGISTRY := $(shell bash -c 'read -p "Enter Docker registry/username: " registry; echo $${registry}') +endif + +DOCKER_IMAGE := $(DOCKER_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG) + +DB_FILE := m3_data/databases/mimic_iv_demo.db + +.PHONY: help +help: ## Show help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' + +.PHONY: all +all: download-db build push ## Complete workflow: download DB, build and push + +.PHONY: login +login: ## Login to Docker Hub + @$(DOCKER) login docker.io + +.PHONY: download-db +download-db: ## Download MIMIC-IV demo database + @uv sync + @uv run m3 init mimic-iv-demo + +.PHONY: build +build: ## Build Docker image (lite version) + @test -f $(DB_FILE) || { echo "Run 'make download-db' first"; exit 1; } + @$(DOCKER) build --target lite -t $(DOCKER_IMAGE) -t $(DOCKER_REGISTRY)/$(IMAGE_NAME):lite . + +.PHONY: build-bigquery +build-bigquery: ## Build BigQuery version + @test -f $(DB_FILE) || { echo "Run 'make download-db' first"; exit 1; } + @$(DOCKER) build --target bigquery -t $(DOCKER_REGISTRY)/$(IMAGE_NAME):bigquery . + +.PHONY: push +push: ## Push Docker image to registry (run 'make login' first) + @$(DOCKER) push $(DOCKER_IMAGE) + @$(DOCKER) push $(DOCKER_REGISTRY)/$(IMAGE_NAME):lite + +.PHONY: push-bigquery +push-bigquery: ## Push BigQuery image (run 'make login' first) + @$(DOCKER) push $(DOCKER_REGISTRY)/$(IMAGE_NAME):bigquery + +.PHONY: test-image +test-image: ## Test the built Docker image + @$(DOCKER) run --rm $(DOCKER_IMAGE) python -c "import m3; print(f'M3 version: {m3.__version__}')" + +.PHONY: clean +clean: ## Remove database and raw files + @rm -rf m3_data + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index 694f801..7703ef3 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,81 @@ m3-mcp-server - 🔐 **Enhanced Security**: Role-based access control, audit logging - 🌐 **Multi-tenant Support**: Organization-level data isolation +## 🐳 Kubernetes Deployment + +Deploy M3 on Kubernetes using Docker images with pre-loaded MIMIC-IV demo database: + +```bash +# Build and push Docker image +make all # Will prompt for Docker registry/username + +# Or specify registry directly +make all DOCKER_REGISTRY=your-username DOCKER=podman +``` + +The container uses StreamableHTTP transport on port 3000 with path `/sse`. Configure your MCP client to connect to the service endpoint (e.g., `http://m3.kagent.svc.cluster.local:3000/sse` for intra-cluster access). + +Helm charts for deploying M3 are available in a separate repository. + +## 🤖 AI Agent Integration + +### Agent Instructions + +Copy these instructions into your AI agent configuration for optimal MIMIC-IV querying: + +**Core Workflow:** +1. Always start with schema discovery using get_database_schema() +2. Use get_table_info(table_name) to see columns and sample data +3. Check sample data for actual formats (dates, column names, values) +4. Write queries with proper JOINs and LIMIT clauses +5. Provide context and interpretation with results + +**Key Tables:** +- patients: Demographics (subject_id, gender, anchor_age, anchor_year) +- admissions: Hospital stays (hadm_id, admittime, dischtime, admission_type) +- icustays: ICU episodes (stay_id, intime, outtime, los) +- labevents: Lab results (itemid, value, valuenum, valueuom) +- prescriptions: Medications (drug, dose_val_rx, route) + +**Best Practices:** +- Always use LIMIT to prevent returning too many rows +- Verify column names from sample data (e.g., 'anchor_age' not 'age') +- Handle NULLs explicitly in clinical data +- Use convenience functions for common patterns +- Explain results, don't just dump data + +**Example Query Pattern:** +```sql +SELECT p.subject_id, p.gender, a.admittime, a.diagnosis +FROM patients p +INNER JOIN admissions a ON p.subject_id = a.subject_id +WHERE a.admission_type = 'EMERGENCY' +LIMIT 10; +``` + +### Sample Questions + +**Basic Exploration:** +- What tables are available in the database? +- Show me the structure of the patients table +- Give me a sample of 5 rows from the icustays table + +**Patient Analysis:** +- How many patients are in the database? +- Show me the age and gender distribution +- What's the average ICU length of stay? + +**Clinical Queries:** +- Show me patients with diabetes diagnoses +- What are the most common admission types? +- Find patients with both high glucose and kidney problems +- Compare ICU length of stay between emergency and elective admissions + +**Deep Dives:** +- Give me the complete medical history for patient 10001 +- Show all ICU stays, diagnoses, and medications for patient 10006 +- What were the lab trends during a patient's last admission? + ## 🤝 Contributing We welcome contributions! Please: diff --git a/src/m3/mcp_server.py b/src/m3/mcp_server.py index cc8bd91..14c2f9c 100644 --- a/src/m3/mcp_server.py +++ b/src/m3/mcp_server.py @@ -682,8 +682,19 @@ def get_race_distribution(limit: int = 10) -> str: def main(): """Main entry point for MCP server.""" - # Run the FastMCP server - mcp.run() + # Check if we should run in HTTP mode (for Kubernetes/Docker) + transport = os.getenv("MCP_TRANSPORT", "stdio").lower() + + if transport in ("sse", "http"): + # Run in HTTP mode for web-based clients (like kagent) + host = os.getenv("MCP_HOST", "0.0.0.0") + port = int(os.getenv("MCP_PORT", "3000")) + path = os.getenv("MCP_PATH", "/sse") + # FastMCP streamable-http transport + mcp.run(transport="streamable-http", host=host, port=port, path=path) + else: + # Run in STDIO mode (default for desktop clients) + mcp.run() if __name__ == "__main__": From 7b76c0103ea60d00a8065f7e4893fd1c5ff7d364 Mon Sep 17 00:00:00 2001 From: Oswaldo Gomez Date: Fri, 28 Nov 2025 22:35:20 +0100 Subject: [PATCH 2/6] Adding proper docstrings --- src/m3/mcp_server.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/m3/mcp_server.py b/src/m3/mcp_server.py index 14c2f9c..dc229be 100644 --- a/src/m3/mcp_server.py +++ b/src/m3/mcp_server.py @@ -681,19 +681,38 @@ def get_race_distribution(limit: int = 10) -> str: def main(): - """Main entry point for MCP server.""" - # Check if we should run in HTTP mode (for Kubernetes/Docker) + """Main entry point for MCP server. + + Initializes and runs the FastMCP server with transport mode determined by environment + variables. Supports both STDIO mode for desktop clients and HTTP mode for web-based + clients and containerized deployments. + + Environment Variables: + MCP_TRANSPORT: Transport mode - "stdio" (default), "sse", or "http" + MCP_HOST: Host to bind to when using HTTP mode (default: "0.0.0.0") + MCP_PORT: Port to bind to when using HTTP mode (default: 3000) + MCP_PATH: Path for SSE endpoint when using HTTP mode (default: "/sse") + + Notes: + Transport Modes: + - STDIO mode (default): Used by desktop MCP clients like Claude Desktop + - HTTP/SSE mode: Used for Kubernetes deployments and web-based clients like kagent + + When MCP_TRANSPORT is set to "http" or "sse", the server runs in streamable-http + mode, which enables HTTP-based communication over Server-Sent Events (SSE). This + is required for containerized deployments where STDIO is not available. + + The server binds to 0.0.0.0 by default in HTTP mode to allow connections from + outside the container, making it accessible via Kubernetes service mesh. + """ transport = os.getenv("MCP_TRANSPORT", "stdio").lower() if transport in ("sse", "http"): - # Run in HTTP mode for web-based clients (like kagent) host = os.getenv("MCP_HOST", "0.0.0.0") port = int(os.getenv("MCP_PORT", "3000")) path = os.getenv("MCP_PATH", "/sse") - # FastMCP streamable-http transport mcp.run(transport="streamable-http", host=host, port=port, path=path) else: - # Run in STDIO mode (default for desktop clients) mcp.run() From f0a723e2546e46e2d54e86fb35a301b73bb2e564 Mon Sep 17 00:00:00 2001 From: Oswaldo Gomez Date: Fri, 28 Nov 2025 22:37:59 +0100 Subject: [PATCH 3/6] Adding a shorter docstring --- src/m3/mcp_server.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/m3/mcp_server.py b/src/m3/mcp_server.py index dc229be..5fc8147 100644 --- a/src/m3/mcp_server.py +++ b/src/m3/mcp_server.py @@ -683,27 +683,18 @@ def get_race_distribution(limit: int = 10) -> str: def main(): """Main entry point for MCP server. - Initializes and runs the FastMCP server with transport mode determined by environment - variables. Supports both STDIO mode for desktop clients and HTTP mode for web-based - clients and containerized deployments. + Runs FastMCP server in either STDIO mode (desktop clients) or HTTP mode + (Kubernetes/web clients). Transport mode configured via environment variables. Environment Variables: - MCP_TRANSPORT: Transport mode - "stdio" (default), "sse", or "http" - MCP_HOST: Host to bind to when using HTTP mode (default: "0.0.0.0") - MCP_PORT: Port to bind to when using HTTP mode (default: 3000) - MCP_PATH: Path for SSE endpoint when using HTTP mode (default: "/sse") + MCP_TRANSPORT: "stdio" (default), "sse", or "http" + MCP_HOST: Host binding for HTTP mode (default: "0.0.0.0") + MCP_PORT: Port for HTTP mode (default: 3000) + MCP_PATH: SSE endpoint path for HTTP mode (default: "/sse") Notes: - Transport Modes: - - STDIO mode (default): Used by desktop MCP clients like Claude Desktop - - HTTP/SSE mode: Used for Kubernetes deployments and web-based clients like kagent - - When MCP_TRANSPORT is set to "http" or "sse", the server runs in streamable-http - mode, which enables HTTP-based communication over Server-Sent Events (SSE). This - is required for containerized deployments where STDIO is not available. - - The server binds to 0.0.0.0 by default in HTTP mode to allow connections from - outside the container, making it accessible via Kubernetes service mesh. + HTTP/SSE mode uses streamable-http transport for containerized deployments + where STDIO is unavailable. Binds to 0.0.0.0 for Kubernetes service mesh access. """ transport = os.getenv("MCP_TRANSPORT", "stdio").lower() From 0760d1a3a473ca1ea9e2614150a9dffa4093aefb Mon Sep 17 00:00:00 2001 From: Oswaldo Gomez Date: Fri, 28 Nov 2025 22:39:49 +0100 Subject: [PATCH 4/6] removing reduntant parts of readme --- README.md | 59 ------------------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/README.md b/README.md index 7703ef3..6614552 100644 --- a/README.md +++ b/README.md @@ -452,65 +452,6 @@ The container uses StreamableHTTP transport on port 3000 with path `/sse`. Confi Helm charts for deploying M3 are available in a separate repository. -## 🤖 AI Agent Integration - -### Agent Instructions - -Copy these instructions into your AI agent configuration for optimal MIMIC-IV querying: - -**Core Workflow:** -1. Always start with schema discovery using get_database_schema() -2. Use get_table_info(table_name) to see columns and sample data -3. Check sample data for actual formats (dates, column names, values) -4. Write queries with proper JOINs and LIMIT clauses -5. Provide context and interpretation with results - -**Key Tables:** -- patients: Demographics (subject_id, gender, anchor_age, anchor_year) -- admissions: Hospital stays (hadm_id, admittime, dischtime, admission_type) -- icustays: ICU episodes (stay_id, intime, outtime, los) -- labevents: Lab results (itemid, value, valuenum, valueuom) -- prescriptions: Medications (drug, dose_val_rx, route) - -**Best Practices:** -- Always use LIMIT to prevent returning too many rows -- Verify column names from sample data (e.g., 'anchor_age' not 'age') -- Handle NULLs explicitly in clinical data -- Use convenience functions for common patterns -- Explain results, don't just dump data - -**Example Query Pattern:** -```sql -SELECT p.subject_id, p.gender, a.admittime, a.diagnosis -FROM patients p -INNER JOIN admissions a ON p.subject_id = a.subject_id -WHERE a.admission_type = 'EMERGENCY' -LIMIT 10; -``` - -### Sample Questions - -**Basic Exploration:** -- What tables are available in the database? -- Show me the structure of the patients table -- Give me a sample of 5 rows from the icustays table - -**Patient Analysis:** -- How many patients are in the database? -- Show me the age and gender distribution -- What's the average ICU length of stay? - -**Clinical Queries:** -- Show me patients with diabetes diagnoses -- What are the most common admission types? -- Find patients with both high glucose and kidney problems -- Compare ICU length of stay between emergency and elective admissions - -**Deep Dives:** -- Give me the complete medical history for patient 10001 -- Show all ICU stays, diagnoses, and medications for patient 10006 -- What were the lab trends during a patient's last admission? - ## 🤝 Contributing We welcome contributions! Please: From 698158a251636560bbee316f67dd8039c76b27b9 Mon Sep 17 00:00:00 2001 From: Oswaldo Gomez Date: Fri, 28 Nov 2025 22:42:16 +0100 Subject: [PATCH 5/6] Bumped minor version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 57b1a76..25d8f4e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile for M3 Docker Image Build and Push DOCKER ?= docker IMAGE_NAME := m3-mimic-demo -IMAGE_TAG ?= 0.0.3 +IMAGE_TAG ?= 0.4.0 # Prompt for registry only if not set ifndef DOCKER_REGISTRY From 72de7f617b4b45f8f5c58fbd90908a26c9a2c062 Mon Sep 17 00:00:00 2001 From: Oswaldo Gomez Date: Fri, 28 Nov 2025 23:25:42 +0100 Subject: [PATCH 6/6] fix(main): Fix formatting --- src/m3/mcp_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/m3/mcp_server.py b/src/m3/mcp_server.py index 65c6f4f..ebc870d 100644 --- a/src/m3/mcp_server.py +++ b/src/m3/mcp_server.py @@ -683,22 +683,22 @@ def get_race_distribution(limit: int = 10) -> str: def main(): """Main entry point for MCP server. - + Runs FastMCP server in either STDIO mode (desktop clients) or HTTP mode (Kubernetes/web clients). Transport mode configured via environment variables. - + Environment Variables: MCP_TRANSPORT: "stdio" (default), "sse", or "http" MCP_HOST: Host binding for HTTP mode (default: "0.0.0.0") MCP_PORT: Port for HTTP mode (default: 3000) MCP_PATH: SSE endpoint path for HTTP mode (default: "/sse") - + Notes: HTTP/SSE mode uses streamable-http transport for containerized deployments where STDIO is unavailable. Binds to 0.0.0.0 for Kubernetes service mesh access. """ transport = os.getenv("MCP_TRANSPORT", "stdio").lower() - + if transport in ("sse", "http"): host = os.getenv("MCP_HOST", "0.0.0.0") port = int(os.getenv("MCP_PORT", "3000"))