-
Notifications
You must be signed in to change notification settings - Fork 768
Enable log notifications from MCP agent server to client #401
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
778c857
Add @app.tool and @app.async_tool decorators
saqadri bccedcd
lint and format
saqadri 874898e
mcp logging notifications
saqadri 27513ad
Fixes for logger
saqadri 29893d1
checkpoint
saqadri 38d23a3
Cleanup
saqadri b94015b
Merge branch 'main' into feat/server_side_notifications
saqadri 0492d8d
post-merge checkpoint
saqadri ec786f1
Working
saqadri 30cfada
Remove custom get-status tool
saqadri 550e7d1
Fix example
saqadri 7ab91f1
Update readmes
saqadri 585155e
Get @app.tool working with Temporal as well
saqadri f7a0476
Fix linter
saqadri 67ffe05
Tests are passing
saqadri 3fc96b9
bump pyproject
saqadri 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
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 |
---|---|---|
|
@@ -13,9 +13,49 @@ https://github.com/user-attachments/assets/f651af86-222d-4df0-8241-616414df66e4 | |
- Creating workflows with the `Workflow` base class | ||
- Registering workflows with an `MCPApp` | ||
- Exposing workflows as MCP tools using `create_mcp_server_for_app`, optionally using custom FastMCP settings | ||
- Preferred: Declaring MCP tools with `@app.tool` and `@app.async_tool` | ||
- Connecting to an MCP server using `gen_client` | ||
- Running workflows remotely and monitoring their status | ||
|
||
## Preferred: Define tools with decorators | ||
|
||
You can declare tools directly from plain Python functions using `@app.tool` (sync) and `@app.async_tool` (async). This is the simplest and recommended way to expose agent logic. | ||
|
||
```python | ||
from mcp_agent.app import MCPApp | ||
from typing import Optional | ||
|
||
app = MCPApp(name="basic_agent_server") | ||
|
||
# Synchronous tool – returns the final result to the caller | ||
@app.tool | ||
async def grade_story(story: str, app_ctx: Optional[Context] = None) -> str: | ||
""" | ||
Grade a student's short story and return a structured report. | ||
""" | ||
# ... implement using your agents/LLMs ... | ||
return "Report..." | ||
|
||
# Asynchronous tool – starts a workflow and returns IDs to poll later | ||
@app.async_tool(name="grade_story_async") | ||
async def grade_story_async(story: str, app_ctx: Optional[Context] = None) -> str: | ||
""" | ||
Start grading the story asynchronously. | ||
|
||
This tool starts the workflow and returns 'workflow_id' and 'run_id'. Use the | ||
generic 'workflows-get_status' tool with the returned IDs to retrieve status/results. | ||
""" | ||
# ... implement using your agents/LLMs ... | ||
return "(async run)" | ||
``` | ||
Comment on lines
+24
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainFix sample: missing Context import and async return shape. Import Context (or remove the annotation) and return IDs from the async tool as documented. ```python
from mcp_agent.app import MCPApp
-from typing import Optional
+from typing import Optional
+from mcp_agent.context import Context # verify correct import path
@@
# Synchronous tool – returns the final result to the caller
@app.tool
async def grade_story(story: str, app_ctx: Optional[Context] = None) -> str:
@@
# Asynchronous tool – starts a workflow and returns IDs to poll later
@app.async_tool(name="grade_story_async")
-async def grade_story_async(story: str, app_ctx: Optional[Context] = None) -> str:
+async def grade_story_async(story: str, app_ctx: Optional[Context] = None) -> dict[str, str]:
@@
- # ... implement using your agents/LLMs ...
- return "(async run)"
+ # ... implement using your agents/LLMs ...
+ return {"workflow_id": "grade_story_async", "run_id": "<generated-run-id>"}
Length of output: 31203 Update Context import and async tool return
🤖 Prompt for AI Agents
|
||
|
||
What gets exposed: | ||
|
||
- Sync tools appear as `<tool_name>` and return the final result (no status polling needed). | ||
- Async tools appear as `<tool_name>` and return `{"workflow_id","run_id"}`; use `workflows-get_status` to query status. | ||
|
||
These decorator-based tools are registered automatically when you call `create_mcp_server_for_app(app)`. | ||
|
||
## Components in this Example | ||
|
||
1. **BasicAgentWorkflow**: A simple workflow that demonstrates basic agent functionality: | ||
|
@@ -34,12 +74,16 @@ https://github.com/user-attachments/assets/f651af86-222d-4df0-8241-616414df66e4 | |
|
||
The MCP agent server exposes the following tools: | ||
|
||
- `workflows-list` - Lists all available workflows | ||
- `workflows-BasicAgentWorkflow-run` - Runs the BasicAgentWorkflow, returns the wf run ID | ||
- `workflows-BasicAgentWorkflow-get_status` - Gets the status of a running workflow | ||
- `workflows-ParallelWorkflow-run` - Runs the ParallelWorkflow, returns the wf run ID | ||
- `workflows-ParallelWorkflow-get_status` - Gets the status of a running workflow | ||
- `workflows-cancel` - Cancels a running workflow | ||
- `workflows-list` - Lists available workflows and their parameter schemas | ||
- `workflows-get_status` - Get status for a running workflow by `run_id` (and optional `workflow_id`) | ||
- `workflows-cancel` - Cancel a running workflow | ||
|
||
If you use the preferred decorator approach: | ||
|
||
- Sync tool: `grade_story` (returns final result) | ||
- Async tool: `grade_story_async` (returns `workflow_id/run_id`; poll with `workflows-get_status`) | ||
|
||
The workflow-based endpoints (e.g., `workflows-<Workflow>-run`) are still available when you define explicit workflow classes. | ||
|
||
## Prerequisites | ||
|
||
|
@@ -55,25 +99,26 @@ Before running the example, you'll need to configure the necessary paths and API | |
|
||
1. Copy the example secrets file: | ||
|
||
```bash | ||
cp mcp_agent.secrets.yaml.example mcp_agent.secrets.yaml | ||
``` | ||
``` | ||
cp mcp_agent.secrets.yaml.example mcp_agent.secrets.yaml | ||
``` | ||
|
||
2. Edit `mcp_agent.secrets.yaml` to add your API keys: | ||
```yaml | ||
anthropic: | ||
api_key: "your-anthropic-api-key" | ||
openai: | ||
api_key: "your-openai-api-key" | ||
``` | ||
|
||
``` | ||
anthropic: | ||
api_key: "your-anthropic-api-key" | ||
openai: | ||
api_key: "your-openai-api-key" | ||
``` | ||
|
||
## How to Run | ||
|
||
### Using the Client Script | ||
|
||
The simplest way to run the example is using the provided client script: | ||
|
||
```bash | ||
``` | ||
# Make sure you're in the mcp_agent_server/asyncio directory | ||
uv run client.py | ||
``` | ||
|
@@ -91,21 +136,52 @@ You can also run the server and client separately: | |
|
||
1. In one terminal, start the server: | ||
|
||
```bash | ||
uv run basic_agent_server.py | ||
``` | ||
uv run basic_agent_server.py | ||
|
||
# Optionally, run with the example custom FastMCP settings | ||
uv run basic_agent_server.py --custom-fastmcp-settings | ||
``` | ||
# Optionally, run with the example custom FastMCP settings | ||
uv run basic_agent_server.py --custom-fastmcp-settings | ||
``` | ||
|
||
2. In another terminal, run the client: | ||
|
||
```bash | ||
uv run client.py | ||
``` | ||
uv run client.py | ||
|
||
# Optionally, run with the example custom FastMCP settings | ||
uv run client.py --custom-fastmcp-settings | ||
``` | ||
|
||
## Receiving Server Logs in the Client | ||
|
||
The server advertises the `logging` capability (via `logging/setLevel`) and forwards its structured logs upstream using `notifications/message`. To receive these logs in a client session, pass a `logging_callback` when constructing the client session and set the desired level: | ||
|
||
```python | ||
from datetime import timedelta | ||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream | ||
from mcp import ClientSession | ||
from mcp.types import LoggingMessageNotificationParams | ||
from mcp_agent.mcp.mcp_agent_client_session import MCPAgentClientSession | ||
|
||
async def on_server_log(params: LoggingMessageNotificationParams) -> None: | ||
print(f"[SERVER LOG] [{params.level.upper()}] [{params.logger}] {params.data}") | ||
|
||
def make_session(read_stream: MemoryObjectReceiveStream, | ||
write_stream: MemoryObjectSendStream, | ||
read_timeout_seconds: timedelta | None) -> ClientSession: | ||
return MCPAgentClientSession( | ||
read_stream=read_stream, | ||
write_stream=write_stream, | ||
read_timeout_seconds=read_timeout_seconds, | ||
logging_callback=on_server_log, | ||
) | ||
|
||
# Optionally, run with the example custom FastMCP settings | ||
uv run client.py --custom-fastmcp-settings | ||
``` | ||
# Later, when connecting via gen_client(..., client_session_factory=make_session) | ||
# you can request the minimum server log level: | ||
# await server.set_logging_level("info") | ||
``` | ||
|
||
The example client (`client.py`) demonstrates this end-to-end: it registers a logging callback and calls `set_logging_level("info")` so logs from the server appear in the client's console. | ||
|
||
## MCP Clients | ||
|
||
|
@@ -116,7 +192,7 @@ like any other MCP server. | |
|
||
You can inspect and test the server using [MCP Inspector](https://github.com/modelcontextprotocol/inspector): | ||
|
||
```bash | ||
``` | ||
npx @modelcontextprotocol/inspector \ | ||
uv \ | ||
--directory /path/to/mcp-agent/examples/mcp_agent_server/asyncio \ | ||
|
@@ -138,41 +214,41 @@ To use this server with Claude Desktop: | |
|
||
2. Add a new server configuration: | ||
|
||
```json | ||
"basic-agent-server": { | ||
"command": "/path/to/uv", | ||
"args": [ | ||
"--directory", | ||
"/path/to/mcp-agent/examples/mcp_agent_server/asyncio", | ||
"run", | ||
"basic_agent_server.py" | ||
] | ||
} | ||
``` | ||
```json | ||
"basic-agent-server": { | ||
"command": "/path/to/uv", | ||
"args": [ | ||
"--directory", | ||
"/path/to/mcp-agent/examples/mcp_agent_server/asyncio", | ||
"run", | ||
"basic_agent_server.py" | ||
] | ||
} | ||
``` | ||
|
||
3. Restart Claude Desktop, and you'll see the server available in the tool drawer | ||
|
||
4. (**claude desktop workaround**) Update `mcp_agent.config.yaml` file with the full paths to npx/uvx on your system: | ||
|
||
Find the full paths to `uvx` and `npx` on your system: | ||
|
||
```bash | ||
which uvx | ||
which npx | ||
``` | ||
|
||
Update the `mcp_agent.config.yaml` file with these paths: | ||
|
||
```yaml | ||
mcp: | ||
servers: | ||
fetch: | ||
command: "/full/path/to/uvx" # Replace with your path | ||
args: ["mcp-server-fetch"] | ||
filesystem: | ||
command: "/full/path/to/npx" # Replace with your path | ||
args: ["-y", "@modelcontextprotocol/server-filesystem"] | ||
``` | ||
Find the full paths to `uvx` and `npx` on your system: | ||
|
||
``` | ||
which uvx | ||
which npx | ||
``` | ||
|
||
Update the `mcp_agent.config.yaml` file with these paths: | ||
|
||
```yaml | ||
mcp: | ||
servers: | ||
fetch: | ||
command: "/full/path/to/uvx" # Replace with your path | ||
args: ["mcp-server-fetch"] | ||
filesystem: | ||
command: "/full/path/to/npx" # Replace with your path | ||
args: ["-y", "@modelcontextprotocol/server-filesystem"] | ||
``` | ||
|
||
## Code Structure | ||
|
||
|
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Align async tool example with described return contract.
Example says it returns workflow_id/run_id but returns a string.
📝 Committable suggestion
🤖 Prompt for AI Agents