-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
109 lines (87 loc) · 3.68 KB
/
Copy pathserver.py
File metadata and controls
109 lines (87 loc) · 3.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import os
from typing import Any
from dotenv import load_dotenv
from fastmcp import FastMCP
import paprika_client
from config import ServerConfig
load_dotenv()
mcp = FastMCP("Paprika")
@mcp.tool()
async def list_recipes() -> list[str]:
"""Return a list of all recipe titles from Paprika."""
await paprika_client._populate_cache()
return [r["name"] for r in paprika_client._recipe_cache.values()]
@mcp.tool()
async def get_recipe(name: str) -> dict | str:
"""Return full details for a recipe by name (case-insensitive, exact match).
Returns a not-found message if no recipe with that name exists."""
await paprika_client._populate_cache()
paprika_client._validate_input_string(name, "name", "get_recipe")
uid = paprika_client._name_index.get(paprika_client._normalize(name))
if uid is None:
return f"No recipe found with name '{name}'."
return paprika_client._recipe_cache[uid]
@mcp.tool()
async def search_recipes(query: str) -> list[str] | str:
"""Search recipes by keyword. Returns all recipe names where every query
token appears in the name (case-insensitive, order-independent)."""
await paprika_client._populate_cache()
paprika_client._validate_input_string(query, "query", "search_recipes")
tokens = paprika_client._normalize(query).split()
matches = [
r["name"]
for r in paprika_client._recipe_cache.values()
if all(token in paprika_client._normalize(r["name"]) for token in tokens)
]
return (
matches
if matches
else (
f"No recipes found matching '{query}' in recipe titles. "
f"Ingredient, source, and natural language search are coming "
f"in a future update. "
f"Try a different title keyword or a more specific term."
)
)
@mcp.tool()
async def sync_recipes(mode: str = "incremental") -> str:
"""Sync the in-memory recipe cache with the Paprika API.
Use 'incremental' by default — fetches only new, edited, or deleted
recipes using hash comparison. Suggest 'full' if the user reports a recipe
is missing or incorrect after an incremental sync, or if the cache may
be partially populated.
Returns a summary of what changed.
"""
paprika_client._validate_input_string(mode, "mode", "sync_recipes")
if mode not in ("incremental", "full"):
raise ValueError("[sync_recipes] 'mode' must be 'incremental' or 'full'.")
result = await paprika_client.sync(mode)
if result.mode == "initial":
return (
f"Cache was empty — performed initial load. {result.total} recipes loaded."
)
if result.mode == "full":
return f"Sync complete (full refresh). Cache contains {result.total} recipes."
return (
f"Sync complete (incremental): {result.added} added, {result.updated} updated,"
f" {result.removed} removed. Cache contains {result.total} recipes."
)
def _run_kwargs(config: ServerConfig) -> dict[str, Any]:
"""Build keyword arguments for FastMCP's run().
Host and port are omitted entirely in stdio mode. FastMCP's run()
forwards **kwargs to run_stdio_async(), which has no host/port
parameters and raises TypeError on unexpected keywords. Omission is
required, not stylistic.
Transport is always passed explicitly rather than left to FastMCP's
default, so a stray FASTMCP_TRANSPORT in .env cannot redirect us.
"""
if config.transport == "stdio":
return {"transport": "stdio"}
return {
"transport": config.transport,
"host": config.host,
"port": config.port,
}
if __name__ == "__main__":
config = ServerConfig.from_env(os.environ)
mcp.run(**_run_kwargs(config))