-
Notifications
You must be signed in to change notification settings - Fork 8
Create an MCP server utilising OpenGIN's Read API endpoints #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
c6ace80
Initial mcp architecture
zaeema-n 96cf94e
Update schema and client
zaeema-n 8591a58
Add toml file
zaeema-n ae04924
Update readme
zaeema-n f654615
Add protobuf decoding
zaeema-n 7833899
Fix protobuf decoding for attributes
zaeema-n 4dc0983
Do not register prompts for now
zaeema-n dcd905c
Fix comment
zaeema-n 56a0fcf
Fix gemini comments
zaeema-n 80f5669
Fix env example file name
zaeema-n d45e5dc
Fix gitignore for .env.example
zaeema-n 206a9e9
Fix readme
zaeema-n d361f7a
Move functions to separate utils file
zaeema-n 1cb147a
Separate start and end time in entity relations tool docstring
zaeema-n 67107eb
Remove unneeded comments
zaeema-n 385e2f9
Fix tool docstrings
zaeema-n File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Copy this file to .env and fill in your values | ||
| OPENGIN_READ_API_URL=http://localhost:8081/v1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| .env | ||
| !.env.example | ||
| __pycache__/ | ||
| *.py[cod] | ||
| *.egg-info/ | ||
| dist/ | ||
| .venv/ | ||
| venv/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,89 @@ | ||
| # MCP Research | ||
| # OpenGIN MCP Server | ||
|
|
||
| A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that provides access to the **OpenGIN Knowledge Graph**. | ||
|
|
||
| This server acts as a bridge between LLMs (like Claude Desktop) and the OpenGIN Read API, allowing AI assistants to query government structures, ministerial appointments, and tabular datasets from Sri Lanka's public records. | ||
|
|
||
| ## Features | ||
|
|
||
| - 📂 **Graph Traversal**: Explore relationships between government entities, ministers, and departments. | ||
| - 🔍 **Universal Search**: Find entities by name, kind, or date. | ||
| - 📊 **Tabular Data Access**: Fetch specific attributes and dataset rows with field-level filtering. | ||
| - 📜 **Schema Exposure**: Built-in schema resource allows the LLM to learn the graph structure dynamically. | ||
|
|
||
| ## Installation | ||
|
|
||
| ### Prerequisites | ||
| - Python 3.11 or higher | ||
| - Access to an OpenGIN Read API instance (default: `http://localhost:8081/v1`) | ||
|
|
||
| ### Setup | ||
| 1. Clone this repository and navigate to the mcp directory. | ||
| 2. Create a virtual environment and install the dependencies: | ||
| ```bash | ||
| pip install -e . | ||
| ``` | ||
| 3. Create a `.env` file in the mcp directory (using `.env.example` as a template): | ||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
| 4. Update `OPENGIN_READ_API_URL` if your API is running somewhere other than localhost. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The server reads the following environment variables: | ||
|
|
||
| | Variable | Default | Description | | ||
| |----------|---------|-------------| | ||
| | `OPENGIN_READ_API_URL` | `http://localhost:8081/v1` | Base URL of the Read API | | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### Tools | ||
| - **`search_entities`**: Find entity IDs by filtering for `kind`, `name`, or `date`. | ||
| - **`get_entity_metadata`**: Fetch the basic record/metadata for a specific entity ID. | ||
| - **`get_entity_attribute`**: Query tabular dataset attributes (like appointments or categories) with row/column filters. | ||
| - **`get_entity_relations`**: Browse outgoing or incoming relationships (e.g., "who is appointed to this ministry?"). | ||
|
|
||
| ### Resources | ||
| - **`opengin://schema`**: Returns the complete OpenGIN Knowledge Graph schema, including entity kinds and relationship types. | ||
|
|
||
| ## Usage with Claude Desktop | ||
|
|
||
| To use this with Claude Desktop, add a new entry to your `claude_desktop_config.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "opengin": { | ||
| "command": "opengin-mcp", | ||
| "cwd": "/absolute/path/to/your/project" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| (Note: If Claude Desktop says it can't find the `opengin-mcp` command, you might need to provide the full path to it. You can find this by running `which opengin-mcp` in your terminal and providing this path as the `command`.) | ||
|
|
||
| ## Running the Server | ||
|
|
||
| ### For Development and Testing | ||
| You can use the **MCP Inspector** to test your tools and resources in a browser-based UI: | ||
| ```bash | ||
| npx @modelcontextprotocol/inspector opengin-mcp | ||
| ``` | ||
|
|
||
| ### Development Mode (Auto-Reload) | ||
| If you have the `fastmcp` CLI tool installed, you can run the server in development mode, which watches for file changes: | ||
| ```bash | ||
| fastmcp dev server.py | ||
| ``` | ||
|
|
||
|
|
||
| ## Project Structure | ||
|
|
||
| - `server.py`: Main entry point initializing the FastMCP server. | ||
| - `client.py`: HTTP client for the OpenGIN Read API. | ||
| - `tools/`: Individual tool implementations (search, metadata, relations, etc.). | ||
| - `resources/`: KG schema definitions. | ||
| - `config.py`: Environment-based configuration management. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| """ | ||
| HTTP client for the OpenGIN Read API. | ||
| Each function maps to one API endpoint. | ||
| """ | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| import httpx | ||
|
|
||
| from config import OPENGIN_READ_API_URL, REQUEST_TIMEOUT | ||
| from utils import decode_protobuf_name, decode_attribute_value, handle_response | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def search_entities( | ||
| *, | ||
| id: str | None = None, | ||
| kind_major: str | None = None, | ||
| kind_minor: str | None = None, | ||
| name: str | None = None, | ||
| created: str | None = None, | ||
| terminated: str | None = None, | ||
| ) -> Any: | ||
| body: dict = {} | ||
| if id: | ||
| body["id"] = id | ||
| else: | ||
| if not kind_major: | ||
| raise ValueError("search_entities: either `id` or `kind_major` is required") | ||
| body["kind"] = {"major": kind_major} | ||
| if kind_minor: | ||
| body["kind"]["minor"] = kind_minor | ||
| if name: | ||
| body["name"] = name | ||
| if created: | ||
| body["created"] = created | ||
| if terminated: | ||
| body["terminated"] = terminated | ||
|
|
||
| with httpx.Client(timeout=REQUEST_TIMEOUT) as client: | ||
| response = client.post(f"{OPENGIN_READ_API_URL}/entities/search", json=body) | ||
|
|
||
| result = handle_response(response, "search_entities") | ||
| for item in result.get("body", []): | ||
| if "name" in item: | ||
| item["name"] = decode_protobuf_name(item["name"]) | ||
| return result | ||
|
|
||
|
|
||
| def get_entity_metadata(entity_id: str) -> Any: | ||
| with httpx.Client(timeout=REQUEST_TIMEOUT) as client: | ||
| response = client.get(f"{OPENGIN_READ_API_URL}/entities/{entity_id}/metadata") | ||
|
|
||
| result = handle_response(response, "get_entity_metadata") | ||
| if isinstance(result, dict) and "name" in result: | ||
| result["name"] = decode_protobuf_name(result["name"]) | ||
| return result | ||
|
|
||
|
|
||
| def get_entity_attribute( | ||
| entity_id: str, | ||
| attribute_name: str, | ||
| *, | ||
| start_time: str | None = None, | ||
| end_time: str | None = None, | ||
| fields: list[str] | None = None, | ||
| ) -> Any: | ||
| params: dict = {} | ||
| if start_time: | ||
| params["startTime"] = start_time | ||
| if end_time: | ||
| params["endTime"] = end_time | ||
| if fields: | ||
| params["fields"] = fields | ||
|
|
||
| url = f"{OPENGIN_READ_API_URL}/entities/{entity_id}/attributes/{attribute_name}" | ||
| with httpx.Client(timeout=REQUEST_TIMEOUT) as client: | ||
| response = client.get(url, params=params) | ||
|
|
||
| result = handle_response(response, "get_entity_attribute") | ||
| if isinstance(result, dict) and "value" in result: | ||
| result["value"] = decode_attribute_value(result["value"]) | ||
| return result | ||
|
|
||
|
|
||
| def get_entity_relations( | ||
| entity_id: str, | ||
| *, | ||
| id: str | None = None, | ||
| related_entity_id: str | None = None, | ||
| name: str | None = None, | ||
| direction: str | None = None, | ||
| active_at: str | None = None, | ||
| start_time: str | None = None, | ||
| end_time: str | None = None, | ||
| ) -> Any: | ||
| if active_at and (start_time or end_time): | ||
| raise ValueError( | ||
| "get_entity_relations: `active_at` and `start_time`/`end_time` are " | ||
| "mutually exclusive — use one or the other, not both." | ||
| ) | ||
|
|
||
| body: dict = {} | ||
| if id: | ||
| body["id"] = id | ||
| else: | ||
| if related_entity_id: | ||
| body["relatedEntityId"] = related_entity_id | ||
| if name: | ||
| body["name"] = name | ||
| if direction: | ||
| body["direction"] = direction | ||
| if active_at: | ||
| body["activeAt"] = active_at | ||
| if start_time: | ||
| body["startTime"] = start_time | ||
| if end_time: | ||
| body["endTime"] = end_time | ||
|
|
||
| url = f"{OPENGIN_READ_API_URL}/entities/{entity_id}/relations" | ||
| with httpx.Client(timeout=REQUEST_TIMEOUT) as client: | ||
| response = client.post(url, json=body) | ||
|
|
||
| result = handle_response(response, "get_entity_relations") | ||
| if isinstance(result, list): | ||
| for item in result: | ||
| if isinstance(item, dict) and "name" in item: | ||
| item["name"] = decode_protobuf_name(item["name"]) | ||
| return result | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| """ | ||
| Configuration for the OpenGIN MCP server. | ||
| All settings are read from environment variables (optionally loaded from .env). | ||
| """ | ||
| import os | ||
| from dotenv import load_dotenv | ||
|
|
||
| load_dotenv() | ||
|
|
||
| # Base URL of the OpenGIN Read API, e.g. http://localhost:8081/v1 | ||
| OPENGIN_READ_API_URL: str = os.environ.get( | ||
| "OPENGIN_READ_API_URL", "http://localhost:8081/v1" | ||
| ).rstrip("/") | ||
|
|
||
| # HTTP request timeout in seconds | ||
| try: | ||
| REQUEST_TIMEOUT: int = int(os.environ.get("OPENGIN_REQUEST_TIMEOUT", "30")) | ||
| except ValueError: | ||
| REQUEST_TIMEOUT: int = 30 # Fallback to default |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from . import explore_entity, trace_relationships, query_attribute | ||
|
|
||
|
|
||
| def register_all(mcp): | ||
| explore_entity.register(mcp) | ||
| trace_relationships.register(mcp) | ||
| query_attribute.register(mcp) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """ | ||
| MCP Prompt: explore_entity | ||
| Given an entity name and kind, find it, fetch its metadata, and survey its relationships. | ||
| """ | ||
|
|
||
|
|
||
| def register(mcp): | ||
| @mcp.prompt() | ||
| def explore_entity(name: str, kind_major: str, kind_minor: str = "") -> str: | ||
| """ | ||
| Given an entity name and kind, find it, then fetch its metadata and key attributes. | ||
| Use this as a starting point for exploring any entity in the knowledge graph. | ||
| """ | ||
| kind_minor_clause = f' and kind_minor="{kind_minor}"' if kind_minor else "" | ||
| return f"""You are exploring an entity in the OpenGIN knowledge graph. | ||
|
|
||
| Steps: | ||
| 1. Call `search_entities` with kind_major="{kind_major}"{kind_minor_clause} and name="{name}" to find the entity ID. | ||
| 2. Call `get_entity_metadata` with the entity ID to understand what this entity is. | ||
| 3. Call `get_entity_relations` with the entity ID (no filters) to see what it is connected to. | ||
| 4. Summarise what you found: the entity's identity, metadata highlights, and key relationships. | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| """ | ||
| MCP Prompt: query_attribute | ||
| Handles the full search → attribute fetch chain for a named entity and attribute. | ||
| """ | ||
|
|
||
|
|
||
| def register(mcp): | ||
| @mcp.prompt() | ||
| def query_attribute( | ||
| entity_name: str, | ||
| kind_major: str, | ||
| attribute_name: str, | ||
| start_time: str = "", | ||
| end_time: str = "", | ||
| ) -> str: | ||
| """ | ||
| Given an entity name and attribute name, handle the full search → fetch chain automatically. | ||
| """ | ||
| time_clause = "" | ||
| if start_time or end_time: | ||
| time_clause = f', start_time="{start_time}", end_time="{end_time}"' | ||
|
|
||
| return f"""You are retrieving attribute data from the OpenGIN knowledge graph. | ||
|
|
||
| Steps: | ||
| 1. Call `search_entities` with kind_major="{kind_major}" and name="{entity_name}" to find the entity ID. | ||
| 2. Call `get_entity_attribute` with the entity ID, attribute_name="{attribute_name}"{time_clause}. | ||
| 3. Summarise the result clearly — mention the time periods covered and the key values returned. | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| """ | ||
| MCP Prompt: trace_relationships | ||
| Walk the relationships of an entity recursively to map its connections. | ||
| """ | ||
|
|
||
|
|
||
| def register(mcp): | ||
| @mcp.prompt() | ||
| def trace_relationships(entity_id: str, direction: str = "OUTGOING", depth: int = 2) -> str: | ||
| """ | ||
| Walk the relationships of an entity recursively to build a picture of its connections. | ||
| """ | ||
| return f"""You are tracing the relationship graph starting from entity "{entity_id}". | ||
|
|
||
| Steps: | ||
| 1. Call `get_entity_relations` with entity_id="{entity_id}" and direction="{direction}" to get its direct relationships. | ||
| 2. For each related entity returned, call `get_entity_relations` on that entity (up to {depth} levels deep). | ||
| 3. Build a summary of how these entities are connected — list each hop and the relationship name connecting them. | ||
| 4. Identify any notable patterns (e.g. a person connected to multiple ministries, or a department with many datasets). | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| [build-system] | ||
| requires = ["setuptools>=61.0"] | ||
| build-backend = "setuptools.build_meta" | ||
|
|
||
| [project] | ||
| name = "opengin-mcp" | ||
| version = "0.1.0" | ||
| description = "A Model Context Protocol (MCP) server for OpenGIN." | ||
| readme = "README.md" | ||
| requires-python = ">=3.11" | ||
| dependencies = [ | ||
| "fastmcp", | ||
| "httpx", | ||
| "python-dotenv", | ||
| "protobuf", | ||
| ] | ||
|
|
||
| [project.scripts] | ||
| opengin-mcp = "server:main" | ||
|
|
||
| [tool.setuptools] | ||
| py-modules = ["server", "client", "config", "utils"] | ||
| packages = ["tools", "prompts", "resources"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import schema | ||
|
|
||
|
|
||
| def register_all(mcp): | ||
| schema.register(mcp) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.