---
title: "Medisolv MCP Creator"
type: "Skill"
slug: "mcp-creator"
icon: "hub"
category: "Platform"
tags: ["Meta", "MCP", "FastMCP", "Python", "Developer Tools", "Platform", "Augment Code", "VSCode"]
installs: "0"
author: "Medisolv Platform Team"
authorInitial: "M"
lastUpdated: "2026-04-02"
popularity: "5.0/5"
reviewCount: "New"
platformTags: ["v1.0", "Internal"]
installLabel: "SKILL.md"
securityBadges:
  - label: "Data Handling"
    status: "No PHI — generates code only"
  - label: "Compliance"
    status: "Internal Developer Use"
---

# Medisolv MCP Creator

Gives any AI agent the complete knowledge needed to scaffold a production-ready Medisolv MCP server from scratch. Install this skill so your agent knows the exact file layout, `pyproject.toml` format, `server.py` template, FastMCP conventions, and portal `README.md` requirements — without you having to explain them every time.

## When to Use

Use this skill when you need to:

- Scaffold a brand-new MCP server under `mcps/` to wrap an internal Medisolv or partner API
- Generate a correct `pyproject.toml` (no `[build-system]` pitfall), `server.py`, and `README.md` in one shot
- Expose a clinical, operational, or identity API as AI-callable tools for Augment Code or Claude Desktop
- Verify that an existing MCP follows the established Medisolv conventions (tool naming, return types, error handling)
- Write the portal catalog `README.md` with correct frontmatter so the Discovery Portal picks it up automatically
- Onboard a new engineer who needs to understand the full MCP authoring recipe without reading every commit

## Tool Definition

```json
{
  "name": "mcp_creator",
  "description": "You are a Medisolv platform engineer who specialises in building FastMCP servers. When asked to create or scaffold an MCP server, always follow the Medisolv conventions exactly as documented in this skill. Generate every required file — pyproject.toml, server.py, README.md, and .env.example — with no steps skipped.",
  "capabilities": [
    "scaffold mcps/<name>/ directory structure",
    "generate pyproject.toml without [build-system]",
    "write server.py from the Medisolv FastMCP template",
    "produce README.md with correct portal frontmatter",
    "produce .env.example listing all required env vars",
    "verify existing MCP servers against Medisolv conventions"
  ],
  "constraints": [
    "Always prefix every tool name with the MCP short name (e.g. citc_, snowflake_)",
    "Always return json.dumps(data, indent=2) — never a raw dict or list",
    "Never add [build-system] to pyproject.toml",
    "Always load .env from the repo root using Path(__file__).resolve().parent.parent.parent / '.env'",
    "Always use absolute paths in mcp.json args[] — relative paths fail at runtime",
    "Always commit uv.lock alongside pyproject.toml",
    "Always quote lastUpdated dates in README.md frontmatter",
    "Always type-annotate every tool parameter — FastMCP generates the JSON schema from annotations",
    "Always use pydantic.Field for per-parameter descriptions on multi-param tools",
    "Always prefer async def for any I/O-bound tool",
    "Always fail fast on missing env vars at startup, not inside tool calls"
  ]
}
```

## Workflow

Follow this lifecycle for every new MCP server:

1. **Capture intent** — clarify which tools/resources/prompts are needed and what APIs or data sources they connect to
2. **Scaffold** — create `mcps/<name>/server.py`, `pyproject.toml`, `README.md`, and `.env.example` using the patterns below
3. **Test** — run `fastmcp dev server.py` (Inspector UI) or the JSON-RPC handshake command to verify all tools
4. **Iterate** — fix issues found in testing, then re-test
5. **Register** — add the MCP config snippet to `mcp.json` so Augment Code in VSCode can call it

---

## What Is an MCP Server?

An MCP (Model Context Protocol) server exposes a set of **tools** that an AI agent can call at runtime — like a REST API, but designed specifically for AI. Each tool has a name, a description, and typed parameters. The AI reads the descriptions and decides when and how to call each tool.

> **Analogy:** Think of an MCP server like a skilled research assistant who sits next to the AI. When the AI needs live data — "What roles does this user have?" — it hands the request to the assistant, who calls the real API and hands back the answer. The AI never touches credentials or HTTP; the MCP handles all of that.

## Technology Stack

All Medisolv MCP servers use:

| Layer | Tool | Why |
| --- | --- | --- |
| Language | Python 3.11+ | Readable, fast to iterate, great async support |
| MCP framework | [FastMCP](https://github.com/jlowin/fastmcp) | Decorators auto-generate JSON schema from type hints |
| HTTP client | `httpx` | Async-native, clean API |
| Env config | `python-dotenv` | Loads `.env` automatically |
| Package manager | `uv` | Single command install, reproducible lockfile |

## File Layout

Every MCP lives in its own directory under `mcps/`:

```
mcps/
  your-mcp-name/
    server.py           ← the MCP implementation (required)
    pyproject.toml      ← Python dependencies (required)
    uv.lock             ← generated by uv sync, commit this
    README.md           ← portal catalog card + setup docs (required)
    .env.example        ← template for required env vars (required)
    .env                ← local secrets — never commit (gitignored)
    .venv/              ← created by uv sync — never commit (gitignored)
```

## pyproject.toml Template

```toml
[project]
name = "your-mcp-name"
version = "1.0.0"
description = "One-line description of what this MCP does"
requires-python = ">=3.11"
dependencies = [
    "fastmcp>=2.0.0",
    "httpx>=0.27.0",
    "python-dotenv>=1.0.0",
]

# Run with: uv run python server.py
```

> **Do not** add a `[build-system]` section. Treating the project as a plain script environment (no build backend) avoids `hatchling` discovery errors when running `uv sync`.

## server.py Template

```python
#!/usr/bin/env python3
"""
Your MCP Name — FastMCP (Python)

One-paragraph description of what API this wraps and what it exposes.

Environment variables (.env file or shell):
  YOUR_BASE_URL     - Base URL of the API  (required)
  YOUR_BEARER_TOKEN - Bearer token for auth (required)
  YOUR_TIMEOUT_MS   - HTTP timeout in ms   (default: 10000)
"""

import json
import os
from pathlib import Path
from typing import Optional

import httpx
from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from pydantic import Field

# ── Config ───────────────────────────────────────────────────────────────────
# Load .env from the repo root (three levels up from this server.py)
load_dotenv(Path(__file__).resolve().parent.parent.parent / ".env")

BASE_URL = os.environ.get("YOUR_BASE_URL", "").rstrip("/")
BEARER   = os.environ.get("YOUR_BEARER_TOKEN", "")
TIMEOUT  = int(os.environ.get("YOUR_TIMEOUT_MS", "10000")) / 1000.0

# Fail fast at startup if required config is missing
if not BASE_URL:
    raise RuntimeError("YOUR_BASE_URL is required. Set it in your .env file.")
if not BEARER:
    raise RuntimeError("YOUR_BEARER_TOKEN is required. Set it in your .env file.")


# ── HTTP helpers ─────────────────────────────────────────────────────────────
def _headers() -> dict[str, str]:
    """Build standard auth headers for every request."""
    return {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": f"Bearer {BEARER}",
    }


async def _get(path: str) -> str:
    """Execute an authenticated GET and return the JSON response as a formatted string."""
    async with httpx.AsyncClient(timeout=TIMEOUT) as client:
        r = await client.get(f"{BASE_URL}{path}", headers=_headers())
        r.raise_for_status()
        return json.dumps(r.json(), indent=2)


async def _post(path: str, body: dict) -> str:
    """Execute an authenticated POST and return the JSON response as a formatted string."""
    async with httpx.AsyncClient(timeout=TIMEOUT) as client:
        r = await client.post(f"{BASE_URL}{path}", headers=_headers(), json=body)
        r.raise_for_status()
        return json.dumps(r.json(), indent=2)


# ── MCP server ────────────────────────────────────────────────────────────────
mcp = FastMCP("your-mcp-name")


# ── Tools ─────────────────────────────────────────────────────────────────────
@mcp.tool
async def your_list_things() -> str:
    """List all things. The docstring becomes the tool description the AI reads."""
    return await _get("/api/things")


@mcp.tool
async def your_get_thing(thing_id: str) -> str:
    """Get a specific thing by its ID."""
    return await _get(f"/api/things/{thing_id}")


@mcp.tool
async def your_search_things(
    query: str = Field(description="The search term"),
    limit: Optional[int] = Field(default=None, description="Max number of results to return"),
) -> str:
    """Search for things matching a query string."""
    params = f"?q={query}" + (f"&limit={limit}" if limit else "")
    return await _get(f"/api/things/search{params}")


# ── Entry point ───────────────────────────────────────────────────────────────
def main() -> None:
    mcp.run()


if __name__ == "__main__":
    main()
```

## Key FastMCP Conventions

### Tool naming
Prefix every tool name with your MCP's short name: `citc_list_applications`, not just `list_applications`. This prevents collisions when multiple MCPs are active in the same session.

### Return type is always `str`
Return `json.dumps(data, indent=2)` — never return raw dicts or lists. The AI reads the string and interprets the JSON.

### Docstrings become tool descriptions
The first line of the docstring is what the AI sees when deciding which tool to call. Make it a complete sentence. Use `Args:` blocks for multi-parameter tools to clarify each argument.

### Use `pydantic.Field` for parameter descriptions
Attach a `description=` to every parameter using `Field` — FastMCP embeds these descriptions into the JSON schema the AI reads when deciding how to call the tool.

```python
from pydantic import Field

@mcp.tool
async def citc_search_applications(
    query: str = Field(description="Search term to filter application names"),
    limit: Optional[int] = Field(default=None, description="Max number of results to return"),
) -> str:
    """Search for CITC applications matching a query string."""
    ...
```

### Optional parameters
Use `Optional[str] = None` (import from `typing`). FastMCP generates the correct nullable schema automatically from the type hint.

### Group tools by domain
Use comment blocks (`# ── Applications ──`) to organize related tools. It has no effect at runtime but makes `server.py` much easier to navigate.

## Resources — Read-Only Data Sources

Resources expose data the AI can read on demand (config snapshots, reference tables, file content). Use `@mcp.resource` for read-only access patterns that don't modify state.

```python
import json
from fastmcp import FastMCP

mcp = FastMCP("your-mcp-name")


# Static resource — URI has no parameters
@mcp.resource("data://config")
def get_config() -> str:
    """Return current server configuration as JSON."""
    return json.dumps({"env": "production", "version": "1.0.0"})


# Template resource — {record_id} in the URI maps to the function argument
@mcp.resource("record://{record_id}/detail")
def get_record_detail(record_id: str) -> str:
    """Return full detail for a record by its ID."""
    detail = {"id": record_id, "status": "active"}  # replace with real fetch
    return json.dumps(detail)
```

**Key rules:**
- Return `str` (text/JSON), `bytes` (binary), or `ResourceResult` for multiple content items
- Template URIs use `{param}` placeholders that map directly to function arguments
- Set `mime_type` explicitly for non-plain-text responses (e.g., `"application/json"`)

---

## Prompts — Reusable Message Templates

Prompts generate structured conversation messages that guide the AI toward a specific response style or task framing.

```python
from fastmcp import FastMCP
from fastmcp.prompts import Message

mcp = FastMCP("your-mcp-name")


@mcp.prompt
def summarize_record(record_type: str, record_json: str) -> list[Message]:
    """Generate a structured summarization request for a clinical or operational record."""
    return [
        # User turn: provide the record to summarize
        Message(f"Please summarize this {record_type} record:\n\n```json\n{record_json}\n```"),
        # Seed the assistant turn to set the right response tone
        Message("I'll summarize the key fields and flag anything that looks unusual.", role="assistant"),
    ]
```

**Key rules:**
- Return `str`, `list[Message]`, or `PromptResult`
- `Message(content, role="user"|"assistant")` wraps individual chat turns
- No `*args` or `**kwargs` — FastMCP needs a fixed parameter schema to expose via MCP

---

## Error Handling

Raise standard Python exceptions for unexpected failures. Use `ToolError` for clean, user-facing messages that won't expose internal stack traces to the AI.

```python
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

mcp = FastMCP("your-mcp-name")


@mcp.tool
async def citc_get_application(
    app_id: str = Field(description="Application ID to retrieve"),
) -> str:
    """Get a CITC application by its ID, raising a clear error if not found."""
    result = await _get(f"/api/applications/{app_id}")
    if not result:
        # ToolError messages are forwarded to the AI as-is — keep them actionable
        raise ToolError(f"Application '{app_id}' was not found. Verify the ID and try again.")
    return result
```

For server-wide masking of internal error details: `FastMCP("Name", mask_error_details=True)`.

---

## README.md Requirements

Every MCP needs a `README.md` the portal renders as a catalog card. It must include:

1. **YAML frontmatter** — same format as a Skill but with `type: "MCP Server"` and `installLabel: "mcp.json"`
2. **What it is** — plain-English explanation of the API being wrapped
3. **When to use** — concrete scenarios
4. **Prerequisites** — Python version, `uv`, required accounts/tokens
5. **Installation** — `uv sync` command
6. **MCP config snippet** — copy-pasteable JSON for `~/.augment/mcp.json`
7. **Available Tools table** — one row per tool with name and description
8. **Example Prompts** — what users can ask once the MCP is running
9. **Troubleshooting** — table of common errors and fixes

## .env.example Template

```env
# Required
YOUR_BASE_URL=https://your-api.example.com
YOUR_BEARER_TOKEN=

# Optional
YOUR_TIMEOUT_MS=10000
```

## Testing the Server

### Option A — Interactive Inspector UI

Use `fastmcp dev` to launch a browser-based debugger that lets you call tools, browse resources and prompts, and inspect inputs/outputs in real time.

```bash
cd mcps/your-mcp-name
uv sync
fastmcp dev server.py
```

### Option B — JSON-RPC Handshake (headless)

Always verify the server handshake before committing — no browser required:

```bash
cd mcps/your-mcp-name
uv sync

printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' \
  | YOUR_BASE_URL=https://api.example.com YOUR_BEARER_TOKEN=test uv run python server.py 2>/dev/null \
  | python3 -c "import sys,json; [print(t['name']) for t in json.loads(sys.stdin.readlines()[1])['result']['tools']]"
```

A working server prints every tool name. A broken one prints nothing or an error.

## MCP Config Snippet (Augment Code)

```json
{
  "mcpServers": {
    "your-mcp-name": {
      "command": "uv",
      "args": ["run", "python", "/absolute/path/to/mcps/your-mcp-name/server.py"],
      "env": {
        "YOUR_BASE_URL": "https://your-api.example.com",
        "YOUR_BEARER_TOKEN": "<your-token>"
      }
    }
  }
}
```

> Use the absolute path to `server.py`. Relative paths fail when the MCP client starts the process from a different working directory.

## Checklist Before Committing

- [ ] `pyproject.toml` has no `[build-system]` section
- [ ] `uv.lock` is committed alongside `pyproject.toml`
- [ ] `.env` and `.venv/` are in `.gitignore`
- [ ] Every tool name is prefixed with the MCP short name
- [ ] Every tool is type-annotated and has a complete one-sentence docstring
- [ ] Multi-parameter tools use `pydantic.Field` with `description=` on every parameter
- [ ] Every tool returns `json.dumps(data, indent=2)`, not a raw dict or list
- [ ] Required env vars fail fast at startup, not inside tool calls
- [ ] Server passes the Inspector UI or JSON-RPC handshake test
- [ ] `mcp.json` entry uses the absolute path to `server.py`
- [ ] `README.md` has correct frontmatter (`type: "MCP Server"`, quoted `lastUpdated`)
- [ ] `.env.example` lists every required environment variable

## Common Mistakes

| Mistake | Fix |
| --- | --- |
| `[build-system]` in `pyproject.toml` | Remove it — `uv sync` treats it as a package and tries to build |
| Bare YAML date in README frontmatter | Quote it: `lastUpdated: "2026-03-26"` |
| Returning a dict from a tool | Wrap in `json.dumps(result, indent=2)` |
| Tool name without prefix | Add the MCP prefix: `citc_` not just `list_` |
| Relative path in `mcp.json` `args` | Use the absolute path to `server.py` |
| Forgetting `uv sync` after editing `pyproject.toml` | Run `uv sync` to regenerate `uv.lock` |
| Missing `Field(description=...)` on parameters | Add `pydantic.Field` so the AI understands each argument |
| Hard-coding secrets in `server.py` | Load from `os.environ` and supply via `mcp.json` `env` block |
| `async def` tool blocking on sync I/O | Convert to `async with httpx.AsyncClient()` or run in a thread pool |
| Using `fastmcp install` for VSCode/Augment Code | Register via `mcp.json` instead — `fastmcp install` targets Claude Desktop |

