Skip to content

Commit a33aea7

Browse files
committed
MCP server: prompts (getting_started, plan_task) and resources (guide, tool catalog); LobeHub badge
1 parent 069f355 commit a33aea7

4 files changed

Lines changed: 130 additions & 2 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Errors come as `<code>: <message>`. `auth_expired` means Tradify signed the sess
6767
| `tradify_api_mcp/app.py` | app name, domains, session cookie, account paths, REST prefix | Rarely |
6868
| `tradify_api_mcp/core/` | `@tool` declaration, errors, the HTTP client with the session (`context.py`), the guard against account changes (`account_guard.py`) | Yes |
6969
| `tradify_api_mcp/session.py` | reading cookies from Chrome over CDP, the session file | Yes |
70-
| `tradify_api_mcp/runner.py`, `api.py`, `mcp_server.py`, `cli.py` | tool registry, REST API, MCP server, command line | Yes |
70+
| `tradify_api_mcp/runner.py`, `api.py`, `mcp_server.py`, `mcp_context.py`, `cli.py` | tool registry, REST API, MCP server, its prompts and resources, command line | Yes |
7171
| `examples/` | scripts for common jobs | Yes |
7272

7373
A new tool is a new file in `tradify_api_mcp/tradify/tools/` with a `@tool(...)` declaration (name, access, input schema, `route`). The registry finds it, and the REST API, the MCP server and the CLI show it without other changes. Every request goes through `_api.Api`: GET for reads, POST with the antiforgery token of the app's start-up answer, and `save` for Breeze `SaveChanges`, which every write uses.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ It is for technical people who want to run it themselves, **for noncommercial us
1313
[![Tradify MCP server](https://img.shields.io/badge/Tradify%20MCP-15%20tools-6d4aef)](#all-15-tools)
1414
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-141318)](#requirements)
1515
[![License: PolyForm Noncommercial 1.0.0](https://img.shields.io/badge/license-PolyForm%20Noncommercial-141318)](LICENSE)
16+
[![MCP Badge](https://lobehub.com/badge/mcp/cotletdev-tradify-api-mcp-server)](https://lobehub.com/mcp/cotletdev-tradify-api-mcp-server)
1617

1718
```bash
1819
git clone https://github.com/cotletdev/tradify-api-mcp-server.git && cd tradify-api-mcp-server

‎tradify_api_mcp/mcp_context.py‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Prompts and resources of the MCP server: how to start a task, and the tool catalog as context."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from collections.abc import Iterable
7+
8+
import mcp.types as types
9+
from mcp.server.lowlevel import Server
10+
from mcp.server.lowlevel.helper_types import ReadResourceContents
11+
from pydantic import AnyUrl
12+
13+
from tradify_api_mcp import app
14+
from tradify_api_mcp.runner import Runner, description, tools
15+
16+
GUIDE_URI = f"{app.SLUG}://guide"
17+
TOOLS_URI = f"{app.SLUG}://tools"
18+
19+
SAFETY = (
20+
"Tools marked write act under the user's name and can reach real people. "
21+
"Before calling one, say exactly what it will send or change and wait for the user's OK."
22+
)
23+
24+
25+
def _guide(runner: Runner) -> str:
26+
specs = tools(runner.read_only)
27+
reads = sum(spec.access == "read" for spec in specs)
28+
mode = "read only" if runner.read_only else "read and write"
29+
return "\n\n".join(
30+
[
31+
f"# {app.NAME} tools",
32+
f"These tools work in the user's own {app.NAME} account through the session of their Chrome "
33+
f"({app.LOGIN_URL}). Mode: {mode}. {len(specs)} tools, {reads} read, {len(specs) - reads} write.",
34+
f"Where to start: {app.AGENT_START}",
35+
SAFETY,
36+
"Lists page with next_cursor: pass it back to get the next page.",
37+
f"The full catalog with each tool's access is in {TOOLS_URI}.",
38+
]
39+
)
40+
41+
42+
def _catalog(runner: Runner) -> str:
43+
return json.dumps(
44+
[
45+
{"name": spec.name, "title": spec.title, "access": spec.access, "description": description(spec)}
46+
for spec in tools(runner.read_only)
47+
],
48+
ensure_ascii=False,
49+
indent=2,
50+
)
51+
52+
53+
def _prompts() -> list[types.Prompt]:
54+
return [
55+
types.Prompt(
56+
name="getting_started",
57+
title=f"Get started with {app.NAME}",
58+
description=f"Check the {app.NAME} session and show what the tools can do, without changing anything.",
59+
),
60+
types.Prompt(
61+
name="plan_task",
62+
title=f"Plan a {app.NAME} task",
63+
description=f"Turn a goal into {app.NAME} tool calls: reads first, every write shown before it runs.",
64+
arguments=[
65+
types.PromptArgument(name="task", description=f"What you want done in {app.NAME}", required=True)
66+
],
67+
),
68+
]
69+
70+
71+
def _prompt_text(name: str, arguments: dict[str, str] | None) -> str:
72+
if name == "getting_started":
73+
return (
74+
f"Use the {app.NAME} tools. {app.AGENT_START} Tell me which account and data you can see, "
75+
f"then list five things I can do with these tools. Read only: change nothing."
76+
)
77+
if name == "plan_task":
78+
task = (arguments or {}).get("task", "").strip()
79+
if not task:
80+
raise ValueError("plan_task needs a task")
81+
return (
82+
f"I want to do this in {app.NAME}: {task}\n\n"
83+
f"Read {TOOLS_URI}, pick the tools, and run the read tools you need first. {SAFETY}"
84+
)
85+
raise ValueError(f"unknown prompt: {name}")
86+
87+
88+
def register(server: Server, runner: Runner) -> None:
89+
@server.list_prompts()
90+
async def list_prompts() -> list[types.Prompt]:
91+
return _prompts()
92+
93+
@server.get_prompt()
94+
async def get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:
95+
text = _prompt_text(name, arguments)
96+
return types.GetPromptResult(
97+
description=next(prompt.description for prompt in _prompts() if prompt.name == name),
98+
messages=[types.PromptMessage(role="user", content=types.TextContent(type="text", text=text))],
99+
)
100+
101+
@server.list_resources()
102+
async def list_resources() -> list[types.Resource]:
103+
return [
104+
types.Resource(
105+
uri=AnyUrl(GUIDE_URI),
106+
name="guide",
107+
title=f"How to use the {app.NAME} tools",
108+
description="Where to start, read and write rules, paging.",
109+
mimeType="text/markdown",
110+
),
111+
types.Resource(
112+
uri=AnyUrl(TOOLS_URI),
113+
name="tools",
114+
title=f"{app.NAME} tool catalog",
115+
description="Every tool with its access (read or write) and full description.",
116+
mimeType="application/json",
117+
),
118+
]
119+
120+
@server.read_resource()
121+
async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]:
122+
if str(uri) == GUIDE_URI:
123+
return [ReadResourceContents(content=_guide(runner), mime_type="text/markdown")]
124+
if str(uri) == TOOLS_URI:
125+
return [ReadResourceContents(content=_catalog(runner), mime_type="application/json")]
126+
raise ValueError(f"unknown resource: {uri}")

‎tradify_api_mcp/mcp_server.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from mcp import types
99
from mcp.server.lowlevel import Server
1010

11-
from tradify_api_mcp import __version__, app
11+
from tradify_api_mcp import __version__, app, mcp_context
1212
from tradify_api_mcp.core.errors import ToolError
1313
from tradify_api_mcp.runner import Runner, description, tools
1414

@@ -55,6 +55,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextCont
5555
raise RuntimeError(f"{exc.code}: {exc}") from None
5656
return [types.TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
5757

58+
mcp_context.register(server, runner)
5859
return server
5960

6061

0 commit comments

Comments
 (0)