Skip to content

Commit ca68329

Browse files
update mcp dependency; add ability to add icons (lastmile-ai#560)
* update mcp dependency; add ability to add icons * fix linter issue * fix test cases * review comments / cleanup / tests * use icon from s3; fix example icon * fix linting issues
1 parent 8d9b6bb commit ca68329

File tree

7 files changed

+202
-42
lines changed

7 files changed

+202
-42
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,36 @@ lost_baggage = SwarmAgent(
592592
593593
</details>
594594
595+
### Icons
596+
597+
You can add icons to your agents for better visualization in UIs that support it (e.g. Claude Desktop).
598+
To do so, you can add an `icons` parameter to your `MCPApp` or `@app.tool` definitions.
599+
600+
```python
601+
# load an icon from file and serve it as a data URI
602+
icon_path = Path("my-icon.png")
603+
icon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()
604+
icon_data_uri = f"data:image/png;base64,{icon_data}"
605+
icon = Icon(src=icon_data_uri, mimeType="image/png", sizes=["64x64"])
606+
607+
# alternatively, one can use an external URL:
608+
web_icon = Icon(src="https://example.com/my-icon.png", mimeType="image/png", sizes=["64x64"])
609+
610+
app = MCPApp(
611+
name="my_app_with_icon",
612+
description="Agent server with an icon",
613+
icons=[icon],
614+
# ...
615+
)
616+
617+
618+
@app.tool(icons=[icon])
619+
async def my_tool(...) -> ...:
620+
# ...
621+
```
622+
623+
If you inject your own `FastMCP` instance into an `MCPApp`, you will have to add your icons there.
624+
595625
### App Config
596626
597627
Create an [`mcp_agent.config.yaml`](/schema/mcp-agent.config.schema.json) and define secrets via either a gitignored [`mcp_agent.secrets.yaml`](./examples/basic/mcp_basic_agent/mcp_agent.secrets.yaml.example) or a local [`.env`](./examples/basic/mcp_basic_agent/.env.example). In production, prefer `MCP_APP_SETTINGS_PRELOAD` to avoid writing plaintext secrets to disk.
8.78 KB
Loading

examples/mcp_agent_server/temporal/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
"""
1010

1111
import asyncio
12+
import base64
1213
import logging
1314
import os
15+
from pathlib import Path
1416

1517
from mcp.types import Icon, ModelHint, ModelPreferences, SamplingMessage, TextContent
1618
from temporalio.exceptions import ApplicationError
@@ -93,12 +95,17 @@ async def run(
9395
return WorkflowResult(value=result)
9496

9597

98+
icon_file = Path(__file__).parent / "mag.png"
99+
icon_data = base64.standard_b64encode(icon_file.read_bytes()).decode()
100+
icon_data_uri = f"data:image/png;base64,{icon_data}"
101+
mag_icon = Icon(src=icon_data_uri, mimeType="image/png", sizes=["64x64"])
102+
96103
@app.tool(
97104
name="finder_tool",
98105
title="Finder Tool",
99106
description="Run the Finder workflow synchronously.",
100107
annotations={"idempotentHint": False},
101-
icons=[Icon(src="emoji:mag")],
108+
icons=[mag_icon],
102109
meta={"category": "demo", "engine": "temporal"},
103110
structured_output=False,
104111
)

src/mcp_agent/app.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from mcp import ServerSession
2424
from mcp.server.fastmcp import FastMCP
2525
from mcp.types import ToolAnnotations, Icon
26-
2726
from mcp_agent.core.context import Context, initialize_context, cleanup_context
2827
from mcp_agent.config import Settings, get_settings
2928
from mcp_agent.executor.signal_registry import SignalRegistry
@@ -56,6 +55,11 @@
5655
P = ParamSpec("P")
5756
R = TypeVar("R")
5857

58+
phetch = Icon(
59+
src="https://s3.us-east-1.amazonaws.com/publicdata.lastmileai.com/phetch.png",
60+
mimeType="image/png",
61+
sizes=["48x48"],
62+
)
5963

6064
class MCPApp:
6165
"""
@@ -89,6 +93,7 @@ def __init__(
8993
signal_notification: SignalWaitCallback | None = None,
9094
upstream_session: Optional["ServerSession"] = None,
9195
model_selector: ModelSelector | None = None,
96+
icons: list[Icon] | None = None,
9297
session_id: str | None = None,
9398
):
9499
"""
@@ -140,6 +145,10 @@ def __init__(
140145
self._signal_notification = signal_notification
141146
self._upstream_session = upstream_session
142147
self._model_selector = model_selector
148+
if icons:
149+
self._icons = icons
150+
else:
151+
self._icons = [phetch]
143152
self._session_id_override = session_id
144153

145154
self._workflows: Dict[str, Type["Workflow"]] = {} # id to workflow class
@@ -954,6 +963,8 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]:
954963
icons_list.append(Icon(**icon))
955964
else:
956965
raise TypeError("icons entries must be Icon or mapping")
966+
else:
967+
icons_list = [phetch]
957968

958969
meta_payload: Dict[str, Any] | None = None
959970
if meta is not None:
@@ -1062,6 +1073,8 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]:
10621073
icons_list.append(Icon(**icon))
10631074
else:
10641075
raise TypeError("icons entries must be Icon or mapping")
1076+
else:
1077+
icons_list = [phetch]
10651078

10661079
meta_payload: Dict[str, Any] | None = None
10671080
if meta is not None:
@@ -1073,6 +1086,7 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]:
10731086
description=description,
10741087
mark_sync_tool=False,
10751088
)
1089+
10761090
# Defer alias tool registration for run/get_status
10771091
self._declared_tools.append(
10781092
{

src/mcp_agent/data/phetch.png

4.49 KB
Loading

src/mcp_agent/server/app_server.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
MCPAgentServer - Exposes MCPApp as MCP server, and
33
mcp-agent workflows and agents as MCP tools.
44
"""
5-
65
from __future__ import annotations
76

87
import json
@@ -32,7 +31,7 @@
3231
from starlette.requests import Request
3332
from starlette.responses import HTMLResponse, JSONResponse
3433

35-
from mcp_agent.app import MCPApp
34+
from mcp_agent.app import MCPApp, phetch
3635
from mcp_agent.agents.agent import Agent
3736
from mcp_agent.core.context_dependent import ContextDependent
3837
from mcp_agent.executor.workflow import Workflow
@@ -1523,11 +1522,14 @@ async def _internal_human_prompts(request: Request):
15231522
except Exception:
15241523
pass
15251524
else:
1525+
if "icons" not in kwargs and app._icons:
1526+
kwargs["icons"] = app._icons
15261527
if "auth" not in kwargs and effective_auth_settings is not None:
15271528
kwargs["auth"] = effective_auth_settings
15281529
if "token_verifier" not in kwargs and token_verifier is not None:
15291530
kwargs["token_verifier"] = token_verifier
15301531
owns_token_verifier = True
1532+
15311533
mcp = FastMCP(
15321534
name=app.name or "mcp_agent_server",
15331535
# TODO: saqadri (MAC) - create a much more detailed description
@@ -1567,7 +1569,7 @@ async def _set_level(
15671569

15681570
# region Workflow Tools
15691571

1570-
@mcp.tool(name="workflows-list")
1572+
@mcp.tool(name="workflows-list", icons=[phetch])
15711573
def list_workflows(ctx: MCPContext) -> Dict[str, Dict[str, Any]]:
15721574
"""
15731575
List all available workflow types with their detailed information.
@@ -1610,7 +1612,7 @@ def list_workflows(ctx: MCPContext) -> Dict[str, Dict[str, Any]]:
16101612

16111613
return result
16121614

1613-
@mcp.tool(name="workflows-runs-list")
1615+
@mcp.tool(name="workflows-runs-list", icons=[phetch])
16141616
async def list_workflow_runs(
16151617
ctx: MCPContext,
16161618
limit: int = 100,
@@ -1667,7 +1669,7 @@ async def list_workflow_runs(
16671669

16681670
return workflow_statuses
16691671

1670-
@mcp.tool(name="workflows-run")
1672+
@mcp.tool(name="workflows-run", icons=[phetch])
16711673
async def run_workflow(
16721674
ctx: MCPContext,
16731675
workflow_name: str,
@@ -1694,7 +1696,7 @@ async def run_workflow(
16941696
pass
16951697
return await _workflow_run(ctx, workflow_name, run_parameters, **kwargs)
16961698

1697-
@mcp.tool(name="workflows-get_status")
1699+
@mcp.tool(name="workflows-get_status", icons=[phetch])
16981700
async def get_workflow_status(
16991701
ctx: MCPContext,
17001702
run_id: str | None = None,
@@ -1740,7 +1742,7 @@ async def get_workflow_status(
17401742
pass
17411743
return await _workflow_status(ctx, run_id=run_id, workflow_id=workflow_id)
17421744

1743-
@mcp.tool(name="workflows-resume")
1745+
@mcp.tool(name="workflows-resume", icons=[phetch])
17441746
async def resume_workflow(
17451747
ctx: MCPContext,
17461748
run_id: str | None = None,
@@ -1820,7 +1822,7 @@ async def resume_workflow(
18201822

18211823
return result
18221824

1823-
@mcp.tool(name="workflows-cancel")
1825+
@mcp.tool(name="workflows-cancel", icons=[phetch])
18241826
async def cancel_workflow(
18251827
ctx: MCPContext, run_id: str | None = None, workflow_id: str | None = None
18261828
) -> bool:
@@ -2202,6 +2204,7 @@ async def _await_task(task: asyncio.Task):
22022204
annotations = decl.get("annotations")
22032205
icons = decl.get("icons")
22042206
_meta = decl.get("meta")
2207+
22052208
# Bind per-iteration values to avoid late-binding closure bugs
22062209
name_local = name
22072210
wname_local = workflow_name
@@ -2438,6 +2441,7 @@ def _schema_fn_proxy(*args, **kwargs):
24382441

24392442
@mcp.tool(
24402443
name=f"workflows-{workflow_name}-run",
2444+
icons=[phetch],
24412445
description=f"""
24422446
Run the '{workflow_name}' workflow and get a dict with workflow_id and run_id back.
24432447
Workflow Description: {workflow_cls.__doc__}

0 commit comments

Comments
 (0)