-
Notifications
You must be signed in to change notification settings - Fork 165
feat: MCP server enhancements + dimos mcp CLI + agent context (DIM-686, DIM-687) #1451
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
40 commits
Select commit
Hold shift + click to select a range
de485fd
feat(cli): add daemon mode for dimos run (DIM-681)
spomichter ff5094d
fix: address greptile review — fd leak, wrong PID, fabricated log path
spomichter 9cde9ef
feat(cli): add dimos stop and dimos status commands (DIM-682, DIM-684)
spomichter 21f16e7
test: add e2e daemon lifecycle tests with PingPong blueprint
spomichter 3854c8a
fix: rename stderr.log to daemon.log (addresses greptile review)
spomichter d1fbc51
fix: resolve mypy type errors in stop command (DIM-681)
spomichter cf3560f
feat: per-run log directory with unified main.jsonl (DIM-685)
spomichter f1e5a01
fix: migrate existing FileHandlers when set_run_log_dir is called
spomichter 1541993
chore: move daemon tests to dimos/core/ for CI discovery
spomichter 50d456d
chore: mark e2e daemon tests as slow
spomichter 7446f75
test: add CLI integration tests for dimos stop and dimos status (DIM-…
spomichter 0b0caa2
test: add e2e CLI tests against real running blueprint (DIM-682, DIM-…
spomichter 9587115
fix: address paul's review comments
spomichter 5d8b76b
fix: drop daemon.log, redirect all stdio to /dev/null
spomichter 8ce1a9d
fix: restore LOG_BASE_DIR import, remove duplicate set_run_log_dir im…
spomichter ec1a3f4
fix: address remaining paul review comments
spomichter c33bd9b
fix: address all remaining paul review comments
spomichter 6f7c4c5
fix: remove module docstring from test_daemon.py
spomichter 32f2661
feat: MCP server enhancements, dimos mcp CLI, agent context, stress t…
spomichter 9caa3fb
fix: address greptile review on PR #1451
spomichter 1858c66
feat: dimos agent-send CLI + MCP method
spomichter fa3a04f
fix: address greptile review round 2
spomichter 459c09a
feat: module IO introspection via MCP + CLI
spomichter 89364f1
fix: daemon context generation + standalone e2e stress tests
spomichter 5c60adf
refactor: strip module-io, fix greptile review issues 7-13
spomichter 7df0e64
cleanup: remove agent_context.py, fix final greptile nits
spomichter c3b6114
merge: dev into feat/dim-686-mcp-agent-cli
spomichter baa7036
fix: address latest greptile review round
spomichter 0237032
fix: resolve mypy errors in worker.py and stress_test_module
spomichter 3ef29ff
perf: class-scoped MCP fixtures, 125s → 51s test runtime
spomichter 8c9ebd7
fix: resolve remaining CI failures (mypy + all_blueprints)
spomichter 959736c
Merge remote-tracking branch 'origin/dev' into feat/dim-686-mcp-agent…
spomichter 315b201
refactor: McpAdapter class + convert custom methods to @skill tools
spomichter 45929a0
fix: alphabetical order in all_blueprints.py for demo-mcp-stress-test
spomichter 1ebfb70
fix: catch HTTPError in McpAdapter, guard None pid in Worker
spomichter b4ae18a
fix: server_status returns main process PID, not worker PID
spomichter 88bc77c
refactor: use click.ParamType for --arg parsing in mcp call
spomichter 7dd5317
Merge remote-tracking branch 'origin/dev' into feat/dim-686-mcp-agent…
spomichter dd84280
fix: viewer_backend → viewer rename + KeyValueType test fix
spomichter 6cf6f27
fix: mypy arg-type error for KeyValueType dict(args)
spomichter 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,190 @@ | ||
| # Copyright 2025-2026 Dimensional Inc. | ||
| # | ||
| # 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. | ||
|
|
||
| """Lightweight MCP JSON-RPC client adapter. | ||
|
|
||
| ``McpAdapter`` provides a typed Python interface to a running MCP server. | ||
| It is used by: | ||
|
|
||
| * The ``dimos mcp`` CLI commands | ||
| * Integration / e2e tests | ||
| * Any code that needs to talk to a local MCP server | ||
|
|
||
| Usage:: | ||
|
|
||
| adapter = McpAdapter("http://localhost:9990/mcp") | ||
| adapter.wait_for_ready(timeout=10) | ||
| tools = adapter.list_tools() | ||
| result = adapter.call_tool("echo", {"message": "hi"}) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import time | ||
| from typing import Any | ||
| import uuid | ||
|
|
||
| import requests | ||
|
|
||
| from dimos.utils.logging_config import setup_logger | ||
|
|
||
| logger = setup_logger() | ||
|
|
||
| DEFAULT_TIMEOUT = 30 | ||
|
|
||
|
|
||
| class McpError(Exception): | ||
| """Raised when the MCP server returns a JSON-RPC error.""" | ||
|
|
||
| def __init__(self, message: str, code: int | None = None) -> None: | ||
| self.code = code | ||
| super().__init__(message) | ||
|
|
||
|
|
||
| class McpAdapter: | ||
| """Thin JSON-RPC client for a running MCP server.""" | ||
|
|
||
| def __init__(self, url: str | None = None, timeout: int = DEFAULT_TIMEOUT) -> None: | ||
| if url is None: | ||
| from dimos.core.global_config import global_config | ||
|
|
||
| url = f"http://localhost:{global_config.mcp_port}/mcp" | ||
| self.url = url | ||
| self.timeout = timeout | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Low-level JSON-RPC | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def call(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]: | ||
| """Send a JSON-RPC request and return the parsed response. | ||
|
|
||
| Raises ``requests.ConnectionError`` if the server is unreachable. | ||
| """ | ||
| payload: dict[str, Any] = { | ||
| "jsonrpc": "2.0", | ||
| "id": str(uuid.uuid4()), | ||
| "method": method, | ||
| } | ||
| if params: | ||
| payload["params"] = params | ||
|
|
||
| resp = requests.post(self.url, json=payload, timeout=self.timeout) | ||
| try: | ||
| resp.raise_for_status() | ||
| except requests.HTTPError as e: | ||
| raise McpError(f"HTTP {resp.status_code}: {e}") from e | ||
| return resp.json() # type: ignore[no-any-return] | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # MCP standard methods | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def initialize(self) -> dict[str, Any]: | ||
| """Send ``initialize`` and return server info.""" | ||
| return self.call("initialize") | ||
|
|
||
| def list_tools(self) -> list[dict[str, Any]]: | ||
| """Return the list of available tools.""" | ||
| result = self._unwrap(self.call("tools/list")) | ||
| return result.get("tools", []) # type: ignore[no-any-return] | ||
|
|
||
| def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]: | ||
| """Call a tool by name and return the result dict.""" | ||
| return self._unwrap(self.call("tools/call", {"name": name, "arguments": arguments or {}})) | ||
|
|
||
| def call_tool_text(self, name: str, arguments: dict[str, Any] | None = None) -> str: | ||
| """Call a tool and return just the first text content item.""" | ||
| result = self.call_tool(name, arguments) | ||
| content = result.get("content", []) | ||
| if not content: | ||
| return "" | ||
| return content[0].get("text", str(content[0])) # type: ignore[no-any-return] | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Readiness probes | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def wait_for_ready(self, timeout: float = 10.0, interval: float = 0.5) -> bool: | ||
| """Poll until the MCP server responds, or return False on timeout.""" | ||
| deadline = time.monotonic() + timeout | ||
| while time.monotonic() < deadline: | ||
| try: | ||
| resp = requests.post( | ||
| self.url, | ||
| json={"jsonrpc": "2.0", "id": "probe", "method": "initialize"}, | ||
| timeout=2, | ||
| ) | ||
| if resp.status_code == 200: | ||
| return True | ||
| except requests.ConnectionError: | ||
| pass | ||
| time.sleep(interval) | ||
| return False | ||
|
|
||
| def wait_for_down(self, timeout: float = 10.0, interval: float = 0.5) -> bool: | ||
| """Poll until the MCP server stops responding.""" | ||
| deadline = time.monotonic() + timeout | ||
| while time.monotonic() < deadline: | ||
| try: | ||
| requests.post( | ||
| self.url, | ||
| json={"jsonrpc": "2.0", "id": "probe", "method": "initialize"}, | ||
| timeout=1, | ||
| ) | ||
| except (requests.ConnectionError, requests.ReadTimeout): | ||
| return True | ||
| time.sleep(interval) | ||
| return False | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Class methods for discovery | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @classmethod | ||
| def from_run_entry(cls, entry: Any | None = None, timeout: int = DEFAULT_TIMEOUT) -> McpAdapter: | ||
| """Create an adapter from a RunEntry, or discover the latest one. | ||
|
|
||
| Falls back to the default URL if no entry is found. | ||
| """ | ||
| if entry is None: | ||
| from dimos.core.run_registry import list_runs | ||
|
|
||
| runs = list_runs(alive_only=True) | ||
| entry = runs[0] if runs else None | ||
|
|
||
| if entry is not None and hasattr(entry, "mcp_url") and entry.mcp_url: | ||
| return cls(url=entry.mcp_url, timeout=timeout) | ||
|
|
||
| # Fall back to default URL using GlobalConfig port | ||
| from dimos.core.global_config import global_config | ||
|
|
||
| url = f"http://localhost:{global_config.mcp_port}/mcp" | ||
| return cls(url=url, timeout=timeout) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Internals | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @staticmethod | ||
| def _unwrap(response: dict[str, Any]) -> dict[str, Any]: | ||
| """Extract the ``result`` from a JSON-RPC response, raising on error.""" | ||
| if "error" in response: | ||
| err = response["error"] | ||
| msg = err.get("message", str(err)) if isinstance(err, dict) else str(err) | ||
| raise McpError(msg, code=err.get("code") if isinstance(err, dict) else None) | ||
| return response.get("result", {}) # type: ignore[no-any-return] | ||
|
|
||
| def __repr__(self) -> str: | ||
| return f"McpAdapter(url={self.url!r})" | ||
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.