-
Notifications
You must be signed in to change notification settings - Fork 34
Route MCP tool calls to mixer agent apis #201
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
6 commits
Select commit
Hold shift + click to select a range
c6e82ab
Integrate Mixer-side agent APIs under USE_MIXER_AGENT_APIS flag
keyurva a5e9406
Refactor Mixer tools into separate file and add logging decorator
keyurva d7d95a3
Format and lint
keyurva 5ffb337
Document USE_MIXER_AGENT_APIS in .env.sample
keyurva 83b383e
Use real place in tool instructions
keyurva 5f7165a
Rename Mixer to Agent API
keyurva 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
122 changes: 122 additions & 0 deletions
122
packages/datacommons-mcp/datacommons_mcp/agent_api_client.py
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,122 @@ | ||
| # Copyright 2026 Google LLC. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """ | ||
| Client for interacting with the Data Commons Agent API. | ||
| """ | ||
|
|
||
| import logging | ||
| import time | ||
| from collections.abc import Callable | ||
| from functools import wraps | ||
| from typing import Any # noqa: ANN401 | ||
|
|
||
| import httpx | ||
|
|
||
| from datacommons_mcp.exceptions import AgentAPIError | ||
| from datacommons_mcp.version import __version__ | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def log_api_call(func: Callable[..., Any]) -> Callable[..., Any]: # noqa: ANN401 | ||
| """Decorator to log URL, request payload, execution time, and errors for Agent API calls.""" | ||
|
|
||
| @wraps(func) | ||
| async def wrapper( | ||
| self: "AgentAPIClient", | ||
| endpoint: str, | ||
| payload: dict, | ||
| *args: object, | ||
| **kwargs: object, | ||
| ) -> Any: # noqa: ANN401 | ||
| url = f"{self.api_root}/{endpoint}" | ||
| logger.info("AgentAPIClient POST request URL: %s, payload: %s", url, payload) | ||
| start_time = time.perf_counter() | ||
| try: | ||
| result = await func(self, endpoint, payload, *args, **kwargs) | ||
| elapsed_time = time.perf_counter() - start_time | ||
| logger.info( | ||
| "AgentAPIClient POST request to %s completed in %.3f seconds", | ||
| url, | ||
| elapsed_time, | ||
| ) | ||
| return result | ||
| except Exception as e: | ||
| elapsed_time = time.perf_counter() - start_time | ||
| logger.error( | ||
| "AgentAPIClient POST request to %s failed after %.3f seconds with error: %s", | ||
| url, | ||
| elapsed_time, | ||
| e, | ||
| ) | ||
| raise | ||
|
|
||
| return wrapper | ||
|
|
||
|
|
||
| class AgentAPIClient: | ||
| """Async client for interacting with Data Commons agent endpoints.""" | ||
|
|
||
| def __init__(self, api_root: str, api_key: str | None = None) -> None: | ||
| """Initialize the AgentAPIClient. | ||
|
|
||
| Args: | ||
| api_root: The base API root URL (e.g. 'https://api.datacommons.org/v2'). | ||
| api_key: Optional API key for authentication. | ||
| """ | ||
| self.api_root = api_root.rstrip("/") | ||
| self.api_key = api_key | ||
| self.headers = { | ||
| "Content-Type": "application/json", | ||
| "x-surface": f"mcp-{__version__}", | ||
| } | ||
| if api_key: | ||
| self.headers["X-API-Key"] = api_key | ||
| self.timeout = 30.0 # 30 seconds default timeout | ||
| self._client = None | ||
|
|
||
| @property | ||
| def client(self) -> httpx.AsyncClient: | ||
| """Lazily initialize the AsyncClient under the active event loop.""" | ||
| if self._client is None: | ||
| self._client = httpx.AsyncClient(headers=self.headers, timeout=self.timeout) | ||
| return self._client | ||
|
|
||
| @log_api_call | ||
| async def post(self, endpoint: str, payload: dict) -> dict: | ||
| """Perform an asynchronous POST request to the specified endpoint. | ||
|
|
||
| Args: | ||
| endpoint: The API endpoint (e.g. 'agent/get_observations'). | ||
| payload: The dictionary to send as JSON payload. | ||
|
|
||
| Returns: | ||
| The parsed JSON response as a dictionary. | ||
| """ | ||
| url = f"{self.api_root}/{endpoint}" | ||
| try: | ||
| response = await self.client.post(url, json=payload) | ||
| response.raise_for_status() | ||
| return response.json() | ||
| except httpx.HTTPStatusError as e: | ||
| error_msg = f"Agent API call to {endpoint} failed with status {e.response.status_code}" | ||
| raise AgentAPIError( | ||
| error_msg, e.response.status_code, e.response.text | ||
| ) from e | ||
|
|
||
| async def close(self) -> None: | ||
| """Close the underlying HTTP client if it was initialized.""" | ||
| if self._client is not None: | ||
| await self._client.aclose() | ||
| self._client = None | ||
73 changes: 73 additions & 0 deletions
73
packages/datacommons-mcp/datacommons_mcp/agent_api_service.py
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,73 @@ | ||
| # Copyright 2026 Google LLC. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """ | ||
| Service layer for calling Agent APIs. | ||
| """ | ||
|
|
||
| from typing import Any | ||
|
|
||
| from datacommons_mcp.agent_api_client import AgentAPIClient | ||
| from datacommons_mcp.app import app | ||
|
|
||
|
|
||
| def _get_agent_api_client() -> AgentAPIClient: | ||
| """Helper to get the initialized AgentAPIClient, raising RuntimeError if not set.""" | ||
| if app.agent_api_client is None: | ||
| raise RuntimeError( | ||
| "Agent API client is not initialized. Ensure DC_USE_AGENT_API is enabled." | ||
| ) | ||
| return app.agent_api_client | ||
|
|
||
|
|
||
| async def get_observations( | ||
| variable_dcid: str, | ||
| place_dcid: str, | ||
| child_place_type: str | None = None, | ||
| source_override: str | None = None, | ||
| date: str | None = None, | ||
| date_range_start: str | None = None, | ||
| date_range_end: str | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Fetches observations via the Agent API agent/get_observations endpoint.""" | ||
| client = _get_agent_api_client() | ||
| payload = { | ||
| "variable_dcid": variable_dcid, | ||
| "place_dcid": place_dcid, | ||
| "child_place_type": child_place_type, | ||
| "source_override": source_override, | ||
| "date": date, | ||
| "date_range_start": date_range_start, | ||
| "date_range_end": date_range_end, | ||
| } | ||
| return await client.post("agent/get_observations", payload) | ||
|
|
||
|
|
||
| async def search_indicators( | ||
| query: str, | ||
| places: list[str] | None = None, | ||
| parent_place: str | None = None, | ||
| per_search_limit: int = 10, | ||
| *, | ||
| include_topics: bool = True, | ||
| ) -> dict[str, Any]: | ||
| """Searches for indicators via the Agent API agent/search_indicators endpoint.""" | ||
| client = _get_agent_api_client() | ||
| payload = { | ||
| "query": query, | ||
| "places": places or [], | ||
| "parent_place": parent_place, | ||
| "per_search_limit": per_search_limit, | ||
| "include_topics": include_topics, | ||
| } | ||
| return await client.post("agent/search_indicators", payload) |
63 changes: 63 additions & 0 deletions
63
packages/datacommons-mcp/datacommons_mcp/agent_api_tools.py
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,63 @@ | ||
| # Copyright 2026 Google LLC. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """ | ||
| Tool implementations for the Agent API-based Data Commons MCP server. | ||
| """ | ||
|
|
||
| from datacommons_mcp.agent_api_service import ( | ||
| get_observations as agent_api_get_observations, | ||
| ) | ||
| from datacommons_mcp.agent_api_service import ( | ||
| search_indicators as agent_api_search_indicators, | ||
| ) | ||
| from datacommons_mcp.data_models.observations import ObservationDateType | ||
|
|
||
|
|
||
| async def get_observations( | ||
| variable_dcid: str, | ||
| place_dcid: str, | ||
| child_place_type: str | None = None, | ||
| source_override: str | None = None, | ||
| date: str = ObservationDateType.LATEST.value, | ||
| date_range_start: str | None = None, | ||
| date_range_end: str | None = None, | ||
| ) -> dict: | ||
| """Fetches observations for a statistical variable from Data Commons.""" | ||
| return await agent_api_get_observations( | ||
| variable_dcid=variable_dcid, | ||
| place_dcid=place_dcid, | ||
| child_place_type=child_place_type, | ||
| source_override=source_override, | ||
| date=date, | ||
| date_range_start=date_range_start, | ||
| date_range_end=date_range_end, | ||
| ) | ||
|
|
||
|
|
||
| async def search_indicators( | ||
| query: str, | ||
| places: list[str] | None = None, | ||
| parent_place: str | None = None, | ||
| per_search_limit: int = 10, | ||
| *, | ||
| include_topics: bool = True, | ||
| ) -> dict: | ||
| """Searches for indicators (topics and variables) in Data Commons.""" | ||
| return await agent_api_search_indicators( | ||
| query=query, | ||
| places=places, | ||
| parent_place=parent_place, | ||
| per_search_limit=per_search_limit, | ||
| include_topics=include_topics, | ||
| ) |
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
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
Oops, something went wrong.
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.