Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Quick context for AI coding agents

This repository implements the OpenAPI.com MCP gateway (FastAPI + FastMCP). The server acts as a proxy that forwards a client's Bearer token to downstream OpenAPI services and exposes MCP tools implemented under `src/openapi_mcp_server/apis/`.

Key files
- `src/openapi_mcp_server/main.py` — application entry point. Mounts the MCP app and contains HTTP endpoints `/callbacks` and `/status/{request_id}`. Shows how token query params are converted into an Authorization header.
- `src/openapi_mcp_server/mcp_core.py` — central FastMCP instance (`mcp`), helper `make_api_call(ctx, method, url, ...)`, and `processPolling` for handling async callbacks.
- `src/openapi_mcp_server/memory_store.py` — in-memory + Memcached-backed callback result store. Exposes `get_callback_result` and `set_callback_result` and defines `BASE_URL`, `callbackUrl` and memcached config from env vars.
- `src/openapi_mcp_server/apis/` — each module defines one or more tools decorated with `@mcp.tool`. Importing these modules triggers tool registration.

What matters for code changes
- Tools are registered when the `apis` modules are imported by `main.py`. Don't remove those imports unless you know how to re-register tools.
- `make_api_call` relies on extracting the Authorization header from FastMCP internals or `ctx.request_context.request.headers`. Many tools assume the client supplied `Authorization: Bearer <token>` (or `?token=` query param).
- Async APIs use a callback workflow: downstream APIs accept a `callback` object with `url` and `custom` (see `company.py`). Callbacks are saved with `set_callback_result` and read with `get_callback_result`.

Common patterns and code examples
- Registering a tool (example from `company.py`):
- Add `@mcp.tool` above an async function with `ctx: Context` as last arg.
- Use `make_api_call(ctx, "GET", url)` to proxy authenticated requests.
- Creating an async callback request (company full profile):
- Build `custom_context = {"request_id": getSessionHash(ctx), ...}`
- POST to the downstream `.../callback` endpoint with `callback.url = memory store callbackUrl` and `callback.custom = custom_context`.
- Use `processPolling(ctx, request_id, ...)` to wait/poll stored results.

Environment and runtime
- Run locally: `python src/openapi_mcp_server/main.py` (README shows `python main.py` from project root; in this layout use the module path). The server binds to 0.0.0.0:PORT (reads `PORT` env var, default 80).
- Virtualenv: repository uses `uv` in README but `pyproject.toml` shows `requires-python = ">=3.13"`. Note: README states Python 3.9+. If you modify runtime or CI, confirm the correct Python target.
- Memcached configuration is read from `MEMCACHED_HOST`, `MEMCACHED_PORT`, and `X-DEV-VM`/`K_SERVICE` influence defaults. Tests or dev runs can run with a mocked `memory_store` (it falls back to in-process dict on Memcached errors).

Developer workflows (concrete)
- Start server (local):
- Create venv and install requirements (README): `uv venv && source .venv/bin/activate && uv pip install -r requirements.txt`
- Run: `python src/openapi_mcp_server/main.py` (or use `uvicorn` as in `if __name__ == '__main__'`).
- Docker: see `Dockerfile` and README.
- Debugging tips:
- Check printed logs: `main.py` and `mcp_core.make_api_call` print request details and errors.
- Use `/status/{request_id}` to inspect async request state set by callbacks.
- If memcached calls fail, memory fallbacks provide predictable behavior.

Conventions and gotchas for agents
- Do not add or hardcode secrets (tokens/keys) into code — this project explicitly expects the client to supply Bearer tokens. Keep token handling in middleware or test fixtures.
- When adding a new API/tool module:
- Follow the `@mcp.tool` decorator pattern and ensure the module is imported from `main.py` or otherwise registered during startup.
- Prefer to call `make_api_call` for HTTP interactions so the common auth/header extraction is reused.
- Beware `BASE_URL` / `callbackUrl` derivation in `memory_store.py` — it depends on `K_SERVICE` and `X-DEV-VM`. Tests running on CI may need to set these env vars.
- The code expects `ctx` to contain `request_context.request.headers` in some places; use defensive checks when reading headers if you add new code paths.

What I couldn't verify automatically
- Exact developer commands for `uv` usage and environment variants are described in README; confirm whether you prefer `python -m venv` flows or `uv` for reproducibility. Also confirm the Python version (pyproject vs README).

If anything here is unclear or you want additional examples (e.g., a sample new tool, unit test scaffolding, CI snippets), tell me which sections to expand and I will iterate.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__pycache__
.venv
.vscode
.devcontainer
uv.lock

# Generated by reconfigure script
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim
FROM python:3.13-slim

# Imposta la directory di lavoro
WORKDIR /app
Expand Down
130 changes: 130 additions & 0 deletions src/openapi_mcp_server/apis/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,133 @@ async def get_company_IT_legal_forms_list(ctx: Context) -> Any:
url = f"https://{SANDBOX_PREFIX}company.openapi.com/IT-legalforms/"
api_call = make_api_call(ctx, "GET", url)
return api_call

# ============================================================================
# WORLDWIDE / EUROPEAN COUNTRIES ENDPOINTS (GENERIC)
# ============================================================================

@mcp.tool(
annotations={
"title": "European/Worldwide basic company data",
"readOnlyHint": True,
"openWorldHint": False,
"idempotentHint": True
}
)
async def get_company_EU_start(vatCode: str, country_code: str, ctx: Context) -> Any:
"""Returns basic company information for a company in the specified country.
Returns: companyName, vatCode, address, activityStatus, and other basic details.

Args:
vatCode: VAT code or company registration number (e.g., DE123456789, FR12345678901234)
country_code: Two-letter country code (e.g., DE, ES, PT, GB, BE, AT, CH, PL, FR)
"""
country_code = country_code.upper()
url = f"https://{SANDBOX_PREFIX}company.openapi.com/{country_code}-start/{vatCode}"
return make_api_call(ctx, "GET", url)

@mcp.tool(
annotations={
"title": "European/Worldwide advanced company data",
"readOnlyHint": True,
"openWorldHint": False,
"idempotentHint": True
}
)
async def get_company_EU_advanced(vatCode: str, country_code: str, ctx: Context) -> Any:
"""Returns advanced company information for a company in the specified country.
Returns: Detailed company data including financial highlights or extended registry info where available.

Args:
vatCode: VAT code or company registration number
country_code: Two-letter country code (e.g., FR, DE, ES, etc.)
"""
country_code = country_code.upper()
url = f"https://{SANDBOX_PREFIX}company.openapi.com/{country_code}-advanced/{vatCode}"
return make_api_call(ctx, "GET", url)

# ============================================================================
# WORLDWIDE SPECIFIC ENDPOINTS
# ============================================================================

@mcp.tool(
annotations={
"title": "Worldwide basic company data",
"readOnlyHint": True,
"openWorldHint": False,
"idempotentHint": True
}
)
async def get_company_WW_start(vatCode: str, country_code: str, ctx: Context) -> Any:
"""Returns basic company information for a company worldwide.
Args:
vatCode: VAT code or company registration number
country_code: Two-letter country code of the country
"""
url = f"https://{SANDBOX_PREFIX}company.openapi.com/WW-start/{country_code}/{vatCode}"
return make_api_call(ctx, "GET", url)

@mcp.tool(
annotations={
"title": "Worldwide advanced company data",
"readOnlyHint": True,
"openWorldHint": False,
"idempotentHint": True
}
)
async def get_company_WW_advanced(vatCode: str, country_code: str, ctx: Context) -> Any:
"""Returns advanced company information for a company worldwide.
Args:
vatCode: VAT code or company registration number
country_code: Two-letter country code of the country
"""
url = f"https://{SANDBOX_PREFIX}company.openapi.com/WW-advanced/{country_code}/{vatCode}"
return make_api_call(ctx, "GET", url)

# ============================================================================
# FRANCE SPECIFIC ENDPOINTS
# ============================================================================

@mcp.tool(
annotations={
"title": "Search French companies by advanced criteria",
"readOnlyHint": True,
"openWorldHint": False,
"idempotentHint": True
}
)
async def get_company_FR_search(
ctx: Context,
companyName: Union[str, None] = None,
provinceCode: Union[str, None] = None,
skip: Union[int, None, str] = None,
limit: Union[int, None, str] = None,
dataEnrichment: str = "name",
dryRun: Union[int, None, str] = None,
nafCode: Union[str, None] = None,
activityStatus: Union[str, None] = None
) -> Any:
"""Returns a list of French companies based on the search criteria.
Args:
companyName: The name or part of it of a French company (optional).
provinceCode: The department code or region to restrict search (optional).
skip: The number of records to skip for pagination (optional).
limit: The maximum number of results to return (optional, default 10).
dataEnrichment: Enrichment options: start, advanced, name (default is name).
dryRun: Set to 1 to only get count and cost (optional).
nafCode: NAF Activity Code for the company (optional).
activityStatus: Status of the company (optional).
"""
url = f"https://{SANDBOX_PREFIX}company.openapi.com/FR-search?limit={limit if limit else 10}"

if companyName: url += f"&companyName={companyName}"
if provinceCode: url += f"&province={provinceCode}"
if skip: url += f"&skip={skip}"
if dataEnrichment: url += f"&dataEnrichment={dataEnrichment}"
if dryRun: url += f"&dryRun={dryRun}"
if nafCode: url += f"&nafCode={nafCode}"
if activityStatus: url += f"&activityStatus={activityStatus}"

return make_api_call(ctx, "GET", url)


Loading