py-easy-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easy_mcp/__init__.py +28 -0
- easy_mcp/cli.py +128 -0
- easy_mcp/protocol.py +92 -0
- easy_mcp/schema.py +158 -0
- easy_mcp/server.py +444 -0
- py_easy_mcp-0.2.0.dist-info/METADATA +161 -0
- py_easy_mcp-0.2.0.dist-info/RECORD +10 -0
- py_easy_mcp-0.2.0.dist-info/WHEEL +4 -0
- py_easy_mcp-0.2.0.dist-info/entry_points.txt +3 -0
- py_easy_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
easy_mcp/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
easy-mcp: Ultra-lightweight, zero-dependency Python framework for Model Context Protocol (MCP).
|
|
3
|
+
Build MCP servers for Claude in seconds.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from easy_mcp.server import EasyMCP
|
|
7
|
+
from easy_mcp.schema import function_to_tool_schema, function_to_prompt_arguments
|
|
8
|
+
from easy_mcp.protocol import (
|
|
9
|
+
MCPError,
|
|
10
|
+
ParseError,
|
|
11
|
+
InvalidRequestError,
|
|
12
|
+
MethodNotFoundError,
|
|
13
|
+
InvalidParamsError,
|
|
14
|
+
InternalError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.2.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"EasyMCP",
|
|
20
|
+
"function_to_tool_schema",
|
|
21
|
+
"function_to_prompt_arguments",
|
|
22
|
+
"MCPError",
|
|
23
|
+
"ParseError",
|
|
24
|
+
"InvalidRequestError",
|
|
25
|
+
"MethodNotFoundError",
|
|
26
|
+
"InvalidParamsError",
|
|
27
|
+
"InternalError",
|
|
28
|
+
]
|
easy_mcp/cli.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI utilities for easy-mcp.
|
|
3
|
+
Scaffold starter servers, generate Claude Desktop config, and test tools locally.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from easy_mcp import __version__
|
|
12
|
+
|
|
13
|
+
STARTER_TEMPLATE = '''"""
|
|
14
|
+
MCP Server generated with easy-mcp.
|
|
15
|
+
Connects with Claude Desktop and Claude Code.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from easy_mcp import EasyMCP
|
|
19
|
+
|
|
20
|
+
mcp = EasyMCP(name="{name}", version="0.1.0", description="Starter MCP Server")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
def hello_world(name: str = "World") -> str:
|
|
25
|
+
"""Say hello to someone.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name: Name of the person to greet
|
|
29
|
+
"""
|
|
30
|
+
return f"Hello, {{name}}! Welcome to MCP."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
def calculate(expression: str) -> str:
|
|
35
|
+
"""Safely calculate a basic arithmetic expression.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
expression: Simple math expression like '2 + 2 * 10'
|
|
39
|
+
"""
|
|
40
|
+
allowed_chars = set("0123456789+-*/(). ")
|
|
41
|
+
if not all(c in allowed_chars for c in expression):
|
|
42
|
+
return "Error: Only basic numbers and operators (+, -, *, /) are allowed."
|
|
43
|
+
try:
|
|
44
|
+
result = eval(expression, {{"__builtins__": None}}, {{}})
|
|
45
|
+
return f"Result: {{result}}"
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return f"Error evaluating expression: {{e}}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
# If run with --config, prints the Claude Desktop config JSON
|
|
52
|
+
if len(sys.argv) > 1 and sys.argv[1] == "--config":
|
|
53
|
+
mcp.print_claude_config()
|
|
54
|
+
else:
|
|
55
|
+
# Standard MCP stdio mode for Claude Desktop / Claude Code
|
|
56
|
+
mcp.run()
|
|
57
|
+
'''
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
61
|
+
name = args.name or "my-mcp-server"
|
|
62
|
+
filename = args.output or "server.py"
|
|
63
|
+
|
|
64
|
+
if os.path.exists(filename) and not args.force:
|
|
65
|
+
print(f"Error: '{filename}' already exists. Use --force to overwrite.", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
content = STARTER_TEMPLATE.format(name=name)
|
|
69
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
70
|
+
f.write(content)
|
|
71
|
+
|
|
72
|
+
print(f"[easy-mcp] Created starter MCP server: {filename}")
|
|
73
|
+
print(f"\nNext steps:")
|
|
74
|
+
print(f"1. Test your server locally:")
|
|
75
|
+
print(f" python {filename} --config")
|
|
76
|
+
print(f"2. Add this server to your Claude Desktop config (claude_desktop_config.json)!")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_config(args: argparse.Namespace) -> int:
|
|
81
|
+
script_path = os.path.abspath(args.script)
|
|
82
|
+
if not os.path.isfile(script_path):
|
|
83
|
+
print(f"Error: Script '{args.script}' not found.", file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
server_name = os.path.splitext(os.path.basename(script_path))[0]
|
|
87
|
+
cfg = {
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
server_name: {
|
|
90
|
+
"command": "python",
|
|
91
|
+
"args": [script_path]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
print(json.dumps(cfg, indent=2))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def main(argv=None) -> int:
|
|
100
|
+
parser = argparse.ArgumentParser(
|
|
101
|
+
prog="easy-mcp",
|
|
102
|
+
description="Fast, zero-dependency toolkit to build and configure MCP servers for Claude."
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
105
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Available subcommands")
|
|
106
|
+
|
|
107
|
+
# init
|
|
108
|
+
p_init = subparsers.add_parser("init", help="Scaffold a new starter MCP server file")
|
|
109
|
+
p_init.add_argument("name", nargs="?", default="my-tools", help="Name of your MCP server")
|
|
110
|
+
p_init.add_argument("-o", "--output", default="server.py", help="Output file path (default: server.py)")
|
|
111
|
+
p_init.add_argument("-f", "--force", action="store_true", help="Overwrite existing file")
|
|
112
|
+
p_init.set_defaults(func=cmd_init)
|
|
113
|
+
|
|
114
|
+
# config
|
|
115
|
+
p_cfg = subparsers.add_parser("config", help="Generate Claude Desktop config JSON for an existing script")
|
|
116
|
+
p_cfg.add_argument("script", help="Path to your python MCP server script")
|
|
117
|
+
p_cfg.set_defaults(func=cmd_config)
|
|
118
|
+
|
|
119
|
+
args = parser.parse_args(argv)
|
|
120
|
+
if not args.subcommand:
|
|
121
|
+
parser.print_help()
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
return args.func(args)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
sys.exit(main())
|
easy_mcp/protocol.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Context Protocol (MCP) and JSON-RPC 2.0 specifications.
|
|
3
|
+
Standard specification: https://spec.modelcontextprotocol.io/
|
|
4
|
+
Zero external dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
JSONRPC_VERSION = "2.0"
|
|
10
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
11
|
+
|
|
12
|
+
# Standard JSON-RPC Error Codes
|
|
13
|
+
PARSE_ERROR = -32700
|
|
14
|
+
INVALID_REQUEST = -32600
|
|
15
|
+
METHOD_NOT_FOUND = -32601
|
|
16
|
+
INVALID_PARAMS = -32602
|
|
17
|
+
INTERNAL_ERROR = -32603
|
|
18
|
+
|
|
19
|
+
# Standard MCP Methods
|
|
20
|
+
METHOD_INITIALIZE = "initialize"
|
|
21
|
+
METHOD_INITIALIZED = "notifications/initialized"
|
|
22
|
+
METHOD_PING = "ping"
|
|
23
|
+
METHOD_TOOLS_LIST = "tools/list"
|
|
24
|
+
METHOD_TOOLS_CALL = "tools/call"
|
|
25
|
+
METHOD_RESOURCES_LIST = "resources/list"
|
|
26
|
+
METHOD_RESOURCES_READ = "resources/read"
|
|
27
|
+
METHOD_PROMPTS_LIST = "prompts/list"
|
|
28
|
+
METHOD_PROMPTS_GET = "prompts/get"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MCPError(Exception):
|
|
32
|
+
"""Base exception for MCP and JSON-RPC errors."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, code: int, message: str, data: Any = None):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.code = code
|
|
37
|
+
self.message = message
|
|
38
|
+
self.data = data
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ParseError(MCPError):
|
|
42
|
+
def __init__(self, message: str = "Parse error", data: Any = None):
|
|
43
|
+
super().__init__(PARSE_ERROR, message, data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InvalidRequestError(MCPError):
|
|
47
|
+
def __init__(self, message: str = "Invalid Request", data: Any = None):
|
|
48
|
+
super().__init__(INVALID_REQUEST, message, data)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MethodNotFoundError(MCPError):
|
|
52
|
+
def __init__(self, message: str = "Method not found", data: Any = None):
|
|
53
|
+
super().__init__(METHOD_NOT_FOUND, message, data)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class InvalidParamsError(MCPError):
|
|
57
|
+
def __init__(self, message: str = "Invalid params", data: Any = None):
|
|
58
|
+
super().__init__(INVALID_PARAMS, message, data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InternalError(MCPError):
|
|
62
|
+
def __init__(self, message: str = "Internal error", data: Any = None):
|
|
63
|
+
super().__init__(INTERNAL_ERROR, message, data)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def make_jsonrpc_response(request_id: Any, result: Any) -> Dict[str, Any]:
|
|
67
|
+
"""Build a compliant JSON-RPC 2.0 response."""
|
|
68
|
+
return {
|
|
69
|
+
"jsonrpc": JSONRPC_VERSION,
|
|
70
|
+
"id": request_id,
|
|
71
|
+
"result": result
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def make_jsonrpc_error(
|
|
76
|
+
request_id: Any,
|
|
77
|
+
code: int,
|
|
78
|
+
message: str,
|
|
79
|
+
data: Optional[Any] = None
|
|
80
|
+
) -> Dict[str, Any]:
|
|
81
|
+
"""Build a compliant JSON-RPC 2.0 error response."""
|
|
82
|
+
err: Dict[str, Any] = {
|
|
83
|
+
"code": code,
|
|
84
|
+
"message": message
|
|
85
|
+
}
|
|
86
|
+
if data is not None:
|
|
87
|
+
err["data"] = data
|
|
88
|
+
return {
|
|
89
|
+
"jsonrpc": JSONRPC_VERSION,
|
|
90
|
+
"id": request_id,
|
|
91
|
+
"error": err
|
|
92
|
+
}
|
easy_mcp/schema.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automatic JSON Schema generator from Python function signatures and docstrings.
|
|
3
|
+
Zero external dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, get_type_hints
|
|
9
|
+
|
|
10
|
+
TYPE_MAP = {
|
|
11
|
+
str: "string",
|
|
12
|
+
int: "integer",
|
|
13
|
+
float: "number",
|
|
14
|
+
bool: "boolean",
|
|
15
|
+
list: "array",
|
|
16
|
+
dict: "object",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def python_type_to_json_type(py_type: Any) -> str:
|
|
21
|
+
"""Map Python types and type hints to JSON schema types."""
|
|
22
|
+
if py_type in TYPE_MAP:
|
|
23
|
+
return TYPE_MAP[py_type]
|
|
24
|
+
|
|
25
|
+
# Handle typing constructs or strings
|
|
26
|
+
type_str = str(py_type).lower()
|
|
27
|
+
if "str" in type_str:
|
|
28
|
+
return "string"
|
|
29
|
+
if "int" in type_str:
|
|
30
|
+
return "integer"
|
|
31
|
+
if "float" in type_str:
|
|
32
|
+
return "number"
|
|
33
|
+
if "bool" in type_str:
|
|
34
|
+
return "boolean"
|
|
35
|
+
if "list" in type_str or "sequence" in type_str:
|
|
36
|
+
return "array"
|
|
37
|
+
if "dict" in type_str or "mapping" in type_str:
|
|
38
|
+
return "object"
|
|
39
|
+
|
|
40
|
+
return "string"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_docstring(docstring: Optional[str]) -> Tuple[str, Dict[str, str]]:
|
|
44
|
+
"""
|
|
45
|
+
Extract main description and parameter descriptions from docstrings.
|
|
46
|
+
Supports Google, Sphinx, and standard docstrings.
|
|
47
|
+
"""
|
|
48
|
+
if not docstring:
|
|
49
|
+
return "", {}
|
|
50
|
+
|
|
51
|
+
lines = docstring.strip().splitlines()
|
|
52
|
+
desc_lines = []
|
|
53
|
+
param_docs: Dict[str, str] = {}
|
|
54
|
+
|
|
55
|
+
in_args_section = False
|
|
56
|
+
current_param = None
|
|
57
|
+
|
|
58
|
+
for line in lines:
|
|
59
|
+
stripped = line.strip()
|
|
60
|
+
if re.match(r"^(Args|Parameters|Arguments):", stripped, re.IGNORECASE):
|
|
61
|
+
in_args_section = True
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if in_args_section:
|
|
65
|
+
# Check for param line: "param_name: description" or "param_name (type): description"
|
|
66
|
+
m = re.match(r"^([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*\([^)]+\))?:\s*(.+)$", stripped)
|
|
67
|
+
if m:
|
|
68
|
+
current_param = m.group(1)
|
|
69
|
+
param_docs[current_param] = m.group(2).strip()
|
|
70
|
+
elif current_param and stripped:
|
|
71
|
+
# Continuation line
|
|
72
|
+
param_docs[current_param] += " " + stripped
|
|
73
|
+
else:
|
|
74
|
+
desc_lines.append(stripped)
|
|
75
|
+
|
|
76
|
+
main_desc = " ".join(desc_lines).strip()
|
|
77
|
+
return main_desc, param_docs
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def function_to_tool_schema(
|
|
81
|
+
func: Callable,
|
|
82
|
+
name: Optional[str] = None,
|
|
83
|
+
description: Optional[str] = None
|
|
84
|
+
) -> Dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Inspect a Python callable and convert it to an MCP tool definition with JSON Schema inputSchema.
|
|
87
|
+
"""
|
|
88
|
+
tool_name = name or func.__name__
|
|
89
|
+
raw_doc = inspect.getdoc(func) or ""
|
|
90
|
+
main_desc, param_docs = parse_docstring(raw_doc)
|
|
91
|
+
tool_desc = description or main_desc or f"Tool: {tool_name}"
|
|
92
|
+
|
|
93
|
+
sig = inspect.signature(func)
|
|
94
|
+
try:
|
|
95
|
+
type_hints = get_type_hints(func)
|
|
96
|
+
except Exception:
|
|
97
|
+
type_hints = {}
|
|
98
|
+
|
|
99
|
+
properties: Dict[str, Any] = {}
|
|
100
|
+
required: List[str] = []
|
|
101
|
+
|
|
102
|
+
for param_name, param in sig.parameters.items():
|
|
103
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
param_type = type_hints.get(param_name, str)
|
|
107
|
+
json_type = python_type_to_json_type(param_type)
|
|
108
|
+
|
|
109
|
+
prop_def: Dict[str, Any] = {"type": json_type}
|
|
110
|
+
|
|
111
|
+
if param_name in param_docs:
|
|
112
|
+
prop_def["description"] = param_docs[param_name]
|
|
113
|
+
|
|
114
|
+
if param.default is not inspect.Parameter.empty:
|
|
115
|
+
prop_def["default"] = param.default
|
|
116
|
+
else:
|
|
117
|
+
required.append(param_name)
|
|
118
|
+
|
|
119
|
+
properties[param_name] = prop_def
|
|
120
|
+
|
|
121
|
+
input_schema: Dict[str, Any] = {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": properties,
|
|
124
|
+
}
|
|
125
|
+
if required:
|
|
126
|
+
input_schema["required"] = required
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"name": tool_name,
|
|
130
|
+
"description": tool_desc,
|
|
131
|
+
"inputSchema": input_schema
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def function_to_prompt_arguments(func: Callable) -> List[Dict[str, Any]]:
|
|
136
|
+
"""
|
|
137
|
+
Inspect a Python callable and generate MCP prompt arguments list.
|
|
138
|
+
"""
|
|
139
|
+
raw_doc = inspect.getdoc(func) or ""
|
|
140
|
+
_, param_docs = parse_docstring(raw_doc)
|
|
141
|
+
|
|
142
|
+
sig = inspect.signature(func)
|
|
143
|
+
arguments: List[Dict[str, Any]] = []
|
|
144
|
+
|
|
145
|
+
for param_name, param in sig.parameters.items():
|
|
146
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
arg_def: Dict[str, Any] = {
|
|
150
|
+
"name": param_name,
|
|
151
|
+
"required": (param.default is inspect.Parameter.empty),
|
|
152
|
+
}
|
|
153
|
+
if param_name in param_docs:
|
|
154
|
+
arg_def["description"] = param_docs[param_name]
|
|
155
|
+
|
|
156
|
+
arguments.append(arg_def)
|
|
157
|
+
|
|
158
|
+
return arguments
|
easy_mcp/server.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core EasyMCP Server implementation.
|
|
3
|
+
Handles stdio JSON-RPC 2.0 communication with Claude Desktop / Claude Code.
|
|
4
|
+
Implements Model Context Protocol (MCP) 2024-11-05 Specification for Tools, Resources, and Prompts.
|
|
5
|
+
Zero external dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from easy_mcp.protocol import (
|
|
15
|
+
INTERNAL_ERROR,
|
|
16
|
+
INVALID_PARAMS,
|
|
17
|
+
INVALID_REQUEST,
|
|
18
|
+
MCP_PROTOCOL_VERSION,
|
|
19
|
+
METHOD_INITIALIZE,
|
|
20
|
+
METHOD_INITIALIZED,
|
|
21
|
+
METHOD_NOT_FOUND,
|
|
22
|
+
METHOD_PING,
|
|
23
|
+
METHOD_PROMPTS_GET,
|
|
24
|
+
METHOD_PROMPTS_LIST,
|
|
25
|
+
METHOD_RESOURCES_LIST,
|
|
26
|
+
METHOD_RESOURCES_READ,
|
|
27
|
+
METHOD_TOOLS_CALL,
|
|
28
|
+
METHOD_TOOLS_LIST,
|
|
29
|
+
PARSE_ERROR,
|
|
30
|
+
make_jsonrpc_error,
|
|
31
|
+
make_jsonrpc_response,
|
|
32
|
+
)
|
|
33
|
+
from easy_mcp.schema import function_to_prompt_arguments, function_to_tool_schema
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EasyMCP:
|
|
37
|
+
"""
|
|
38
|
+
Lightweight, zero-dependency Model Context Protocol (MCP) Server.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
mcp = EasyMCP("my-server", version="0.2.0")
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
def add(a: float, b: float) -> float:
|
|
45
|
+
'''Add two numbers together.'''
|
|
46
|
+
return a + b
|
|
47
|
+
|
|
48
|
+
@mcp.resource("memo://notes")
|
|
49
|
+
def get_notes() -> str:
|
|
50
|
+
'''System documentation notes.'''
|
|
51
|
+
return "Server documentation and quick guides."
|
|
52
|
+
|
|
53
|
+
@mcp.prompt()
|
|
54
|
+
def code_review(code: str) -> str:
|
|
55
|
+
'''Review code for safety and style.'''
|
|
56
|
+
return f"Please review this code for security issues:\n\n{code}"
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
mcp.run()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
name: str = "easy-mcp-server",
|
|
65
|
+
version: str = "0.2.0",
|
|
66
|
+
description: str = ""
|
|
67
|
+
):
|
|
68
|
+
self.name = name
|
|
69
|
+
self.version = version
|
|
70
|
+
self.description = description
|
|
71
|
+
|
|
72
|
+
self._tools: Dict[str, Callable] = {}
|
|
73
|
+
self._tool_schemas: Dict[str, Dict[str, Any]] = {}
|
|
74
|
+
|
|
75
|
+
self._resources: Dict[str, Callable] = {}
|
|
76
|
+
self._resource_meta: Dict[str, Dict[str, Any]] = {}
|
|
77
|
+
|
|
78
|
+
self._prompts: Dict[str, Callable] = {}
|
|
79
|
+
self._prompt_meta: Dict[str, Dict[str, Any]] = {}
|
|
80
|
+
|
|
81
|
+
# ==========================================
|
|
82
|
+
# Tools API
|
|
83
|
+
# ==========================================
|
|
84
|
+
|
|
85
|
+
def tool(self, name: Optional[str] = None, description: Optional[str] = None):
|
|
86
|
+
"""Decorator to register a Python function as an MCP tool."""
|
|
87
|
+
def decorator(func: Callable):
|
|
88
|
+
self.register_tool(func, name=name, description=description)
|
|
89
|
+
return func
|
|
90
|
+
return decorator
|
|
91
|
+
|
|
92
|
+
def register_tool(
|
|
93
|
+
self,
|
|
94
|
+
func: Callable,
|
|
95
|
+
name: Optional[str] = None,
|
|
96
|
+
description: Optional[str] = None
|
|
97
|
+
):
|
|
98
|
+
"""Programmatic registration of an MCP tool."""
|
|
99
|
+
tool_name = name or func.__name__
|
|
100
|
+
schema = function_to_tool_schema(func, name=tool_name, description=description)
|
|
101
|
+
self._tools[tool_name] = func
|
|
102
|
+
self._tool_schemas[tool_name] = schema
|
|
103
|
+
|
|
104
|
+
def list_tools(self) -> List[Dict[str, Any]]:
|
|
105
|
+
"""Return list of registered MCP tools."""
|
|
106
|
+
return list(self._tool_schemas.values())
|
|
107
|
+
|
|
108
|
+
def call_tool(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
109
|
+
"""Execute a tool locally and return MCP content block result."""
|
|
110
|
+
if name not in self._tools:
|
|
111
|
+
raise KeyError(f"Tool '{name}' not found.")
|
|
112
|
+
|
|
113
|
+
args = arguments or {}
|
|
114
|
+
func = self._tools[name]
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
res = func(**args)
|
|
118
|
+
|
|
119
|
+
if isinstance(res, (dict, list)):
|
|
120
|
+
text_content = json.dumps(res, ensure_ascii=False, indent=2)
|
|
121
|
+
else:
|
|
122
|
+
text_content = str(res)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"content": [
|
|
126
|
+
{
|
|
127
|
+
"type": "text",
|
|
128
|
+
"text": text_content
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
"isError": False
|
|
132
|
+
}
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return {
|
|
135
|
+
"content": [
|
|
136
|
+
{
|
|
137
|
+
"type": "text",
|
|
138
|
+
"text": f"Error executing tool '{name}': {str(e)}\n{traceback.format_exc()}"
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"isError": True
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# ==========================================
|
|
145
|
+
# Resources API
|
|
146
|
+
# ==========================================
|
|
147
|
+
|
|
148
|
+
def resource(
|
|
149
|
+
self,
|
|
150
|
+
uri: str,
|
|
151
|
+
name: Optional[str] = None,
|
|
152
|
+
description: Optional[str] = None,
|
|
153
|
+
mime_type: str = "text/plain"
|
|
154
|
+
):
|
|
155
|
+
"""Decorator to register a function as an MCP resource provider."""
|
|
156
|
+
def decorator(func: Callable):
|
|
157
|
+
self.register_resource(
|
|
158
|
+
uri=uri,
|
|
159
|
+
func=func,
|
|
160
|
+
name=name,
|
|
161
|
+
description=description,
|
|
162
|
+
mime_type=mime_type
|
|
163
|
+
)
|
|
164
|
+
return func
|
|
165
|
+
return decorator
|
|
166
|
+
|
|
167
|
+
def register_resource(
|
|
168
|
+
self,
|
|
169
|
+
uri: str,
|
|
170
|
+
func: Callable,
|
|
171
|
+
name: Optional[str] = None,
|
|
172
|
+
description: Optional[str] = None,
|
|
173
|
+
mime_type: str = "text/plain"
|
|
174
|
+
):
|
|
175
|
+
"""Programmatic registration of an MCP resource."""
|
|
176
|
+
res_name = name or uri
|
|
177
|
+
doc = (func.__doc__ or "").strip()
|
|
178
|
+
res_desc = description or doc or f"Resource: {res_name}"
|
|
179
|
+
|
|
180
|
+
self._resources[uri] = func
|
|
181
|
+
self._resource_meta[uri] = {
|
|
182
|
+
"uri": uri,
|
|
183
|
+
"name": res_name,
|
|
184
|
+
"description": res_desc,
|
|
185
|
+
"mimeType": mime_type,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def list_resources(self) -> List[Dict[str, Any]]:
|
|
189
|
+
"""Return list of registered MCP resources."""
|
|
190
|
+
return list(self._resource_meta.values())
|
|
191
|
+
|
|
192
|
+
def read_resource(self, uri: str) -> Dict[str, Any]:
|
|
193
|
+
"""Read an MCP resource by URI."""
|
|
194
|
+
if uri not in self._resources:
|
|
195
|
+
raise KeyError(f"Resource '{uri}' not found.")
|
|
196
|
+
|
|
197
|
+
meta = self._resource_meta[uri]
|
|
198
|
+
func = self._resources[uri]
|
|
199
|
+
|
|
200
|
+
# Function may take 0 or 1 argument (uri)
|
|
201
|
+
import inspect
|
|
202
|
+
sig = inspect.signature(func)
|
|
203
|
+
if len(sig.parameters) == 1:
|
|
204
|
+
data = func(uri)
|
|
205
|
+
else:
|
|
206
|
+
data = func()
|
|
207
|
+
|
|
208
|
+
if isinstance(data, (dict, list)):
|
|
209
|
+
text_data = json.dumps(data, ensure_ascii=False, indent=2)
|
|
210
|
+
else:
|
|
211
|
+
text_data = str(data)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"contents": [
|
|
215
|
+
{
|
|
216
|
+
"uri": uri,
|
|
217
|
+
"mimeType": meta.get("mimeType", "text/plain"),
|
|
218
|
+
"text": text_data
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# ==========================================
|
|
224
|
+
# Prompts API
|
|
225
|
+
# ==========================================
|
|
226
|
+
|
|
227
|
+
def prompt(
|
|
228
|
+
self,
|
|
229
|
+
name: Optional[str] = None,
|
|
230
|
+
description: Optional[str] = None,
|
|
231
|
+
arguments: Optional[List[Dict[str, Any]]] = None
|
|
232
|
+
):
|
|
233
|
+
"""Decorator to register a prompt template generator."""
|
|
234
|
+
def decorator(func: Callable):
|
|
235
|
+
self.register_prompt(
|
|
236
|
+
func=func,
|
|
237
|
+
name=name,
|
|
238
|
+
description=description,
|
|
239
|
+
arguments=arguments
|
|
240
|
+
)
|
|
241
|
+
return func
|
|
242
|
+
return decorator
|
|
243
|
+
|
|
244
|
+
def register_prompt(
|
|
245
|
+
self,
|
|
246
|
+
func: Callable,
|
|
247
|
+
name: Optional[str] = None,
|
|
248
|
+
description: Optional[str] = None,
|
|
249
|
+
arguments: Optional[List[Dict[str, Any]]] = None
|
|
250
|
+
):
|
|
251
|
+
"""Programmatic registration of an MCP prompt."""
|
|
252
|
+
prompt_name = name or func.__name__
|
|
253
|
+
doc = (func.__doc__ or "").strip()
|
|
254
|
+
prompt_desc = description or doc or f"Prompt: {prompt_name}"
|
|
255
|
+
args_list = arguments if arguments is not None else function_to_prompt_arguments(func)
|
|
256
|
+
|
|
257
|
+
self._prompts[prompt_name] = func
|
|
258
|
+
self._prompt_meta[prompt_name] = {
|
|
259
|
+
"name": prompt_name,
|
|
260
|
+
"description": prompt_desc,
|
|
261
|
+
"arguments": args_list
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
def list_prompts(self) -> List[Dict[str, Any]]:
|
|
265
|
+
"""Return list of registered MCP prompts."""
|
|
266
|
+
return list(self._prompt_meta.values())
|
|
267
|
+
|
|
268
|
+
def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
269
|
+
"""Execute a prompt template and return MCP prompt response."""
|
|
270
|
+
if name not in self._prompts:
|
|
271
|
+
raise KeyError(f"Prompt '{name}' not found.")
|
|
272
|
+
|
|
273
|
+
meta = self._prompt_meta[name]
|
|
274
|
+
func = self._prompts[name]
|
|
275
|
+
args = arguments or {}
|
|
276
|
+
|
|
277
|
+
res = func(**args)
|
|
278
|
+
|
|
279
|
+
# Normalize result into list of messages
|
|
280
|
+
if isinstance(res, list):
|
|
281
|
+
messages = res
|
|
282
|
+
elif isinstance(res, str):
|
|
283
|
+
messages = [
|
|
284
|
+
{
|
|
285
|
+
"role": "user",
|
|
286
|
+
"content": {
|
|
287
|
+
"type": "text",
|
|
288
|
+
"text": res
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
elif isinstance(res, dict) and "messages" in res:
|
|
293
|
+
return res
|
|
294
|
+
else:
|
|
295
|
+
messages = [
|
|
296
|
+
{
|
|
297
|
+
"role": "user",
|
|
298
|
+
"content": {
|
|
299
|
+
"type": "text",
|
|
300
|
+
"text": str(res)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"description": meta.get("description", ""),
|
|
307
|
+
"messages": messages
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# ==========================================
|
|
311
|
+
# JSON-RPC Request Dispatcher
|
|
312
|
+
# ==========================================
|
|
313
|
+
|
|
314
|
+
def handle_request(self, request_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
315
|
+
"""Handle a single JSON-RPC 2.0 request."""
|
|
316
|
+
if not isinstance(request_data, dict):
|
|
317
|
+
return make_jsonrpc_error(None, INVALID_REQUEST, "Invalid JSON-RPC request structure.")
|
|
318
|
+
|
|
319
|
+
req_id = request_data.get("id")
|
|
320
|
+
method = request_data.get("method")
|
|
321
|
+
params = request_data.get("params", {})
|
|
322
|
+
is_notification = (req_id is None)
|
|
323
|
+
|
|
324
|
+
if not method or not isinstance(method, str):
|
|
325
|
+
return make_jsonrpc_error(req_id, INVALID_REQUEST, "Missing or invalid 'method' parameter.")
|
|
326
|
+
|
|
327
|
+
# 1. Initialize
|
|
328
|
+
if method == METHOD_INITIALIZE:
|
|
329
|
+
result = {
|
|
330
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
331
|
+
"capabilities": {
|
|
332
|
+
"tools": {},
|
|
333
|
+
"resources": {},
|
|
334
|
+
"prompts": {}
|
|
335
|
+
},
|
|
336
|
+
"serverInfo": {
|
|
337
|
+
"name": self.name,
|
|
338
|
+
"version": self.version
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return make_jsonrpc_response(req_id, result)
|
|
342
|
+
|
|
343
|
+
elif method == METHOD_INITIALIZED:
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
# 2. Ping
|
|
347
|
+
elif method == METHOD_PING:
|
|
348
|
+
return make_jsonrpc_response(req_id, {})
|
|
349
|
+
|
|
350
|
+
# 3. Tools
|
|
351
|
+
elif method == METHOD_TOOLS_LIST:
|
|
352
|
+
return make_jsonrpc_response(req_id, {"tools": self.list_tools()})
|
|
353
|
+
|
|
354
|
+
elif method == METHOD_TOOLS_CALL:
|
|
355
|
+
tool_name = params.get("name")
|
|
356
|
+
arguments = params.get("arguments", {})
|
|
357
|
+
if not tool_name:
|
|
358
|
+
return make_jsonrpc_error(req_id, INVALID_PARAMS, "Missing required parameter: 'name'")
|
|
359
|
+
if tool_name not in self._tools:
|
|
360
|
+
return make_jsonrpc_error(req_id, METHOD_NOT_FOUND, f"Tool '{tool_name}' not found.")
|
|
361
|
+
|
|
362
|
+
res = self.call_tool(tool_name, arguments)
|
|
363
|
+
return make_jsonrpc_response(req_id, res)
|
|
364
|
+
|
|
365
|
+
# 4. Resources
|
|
366
|
+
elif method == METHOD_RESOURCES_LIST:
|
|
367
|
+
return make_jsonrpc_response(req_id, {"resources": self.list_resources()})
|
|
368
|
+
|
|
369
|
+
elif method == METHOD_RESOURCES_READ:
|
|
370
|
+
uri = params.get("uri")
|
|
371
|
+
if not uri:
|
|
372
|
+
return make_jsonrpc_error(req_id, INVALID_PARAMS, "Missing required parameter: 'uri'")
|
|
373
|
+
if uri not in self._resources:
|
|
374
|
+
return make_jsonrpc_error(req_id, INVALID_PARAMS, f"Resource '{uri}' not found.")
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
res = self.read_resource(uri)
|
|
378
|
+
return make_jsonrpc_response(req_id, res)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
return make_jsonrpc_error(req_id, INTERNAL_ERROR, f"Error reading resource: {str(e)}")
|
|
381
|
+
|
|
382
|
+
# 5. Prompts
|
|
383
|
+
elif method == METHOD_PROMPTS_LIST:
|
|
384
|
+
return make_jsonrpc_response(req_id, {"prompts": self.list_prompts()})
|
|
385
|
+
|
|
386
|
+
elif method == METHOD_PROMPTS_GET:
|
|
387
|
+
prompt_name = params.get("name")
|
|
388
|
+
arguments = params.get("arguments", {})
|
|
389
|
+
if not prompt_name:
|
|
390
|
+
return make_jsonrpc_error(req_id, INVALID_PARAMS, "Missing required parameter: 'name'")
|
|
391
|
+
if prompt_name not in self._prompts:
|
|
392
|
+
return make_jsonrpc_error(req_id, METHOD_NOT_FOUND, f"Prompt '{prompt_name}' not found.")
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
res = self.get_prompt(prompt_name, arguments)
|
|
396
|
+
return make_jsonrpc_response(req_id, res)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
return make_jsonrpc_error(req_id, INTERNAL_ERROR, f"Error generating prompt: {str(e)}")
|
|
399
|
+
|
|
400
|
+
else:
|
|
401
|
+
if is_notification:
|
|
402
|
+
return None
|
|
403
|
+
return make_jsonrpc_error(req_id, METHOD_NOT_FOUND, f"Unknown method: '{method}'")
|
|
404
|
+
|
|
405
|
+
def run_stdio(self):
|
|
406
|
+
"""Run the stdio JSON-RPC loop connecting with Claude Desktop."""
|
|
407
|
+
for line in sys.stdin:
|
|
408
|
+
line = line.strip()
|
|
409
|
+
if not line:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
request_data = json.loads(line)
|
|
414
|
+
except json.JSONDecodeError as e:
|
|
415
|
+
err_resp = make_jsonrpc_error(None, PARSE_ERROR, f"JSON parse error: {str(e)}")
|
|
416
|
+
sys.stdout.write(json.dumps(err_resp) + "\n")
|
|
417
|
+
sys.stdout.flush()
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
response = self.handle_request(request_data)
|
|
421
|
+
if response is not None:
|
|
422
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
423
|
+
sys.stdout.flush()
|
|
424
|
+
|
|
425
|
+
def run(self):
|
|
426
|
+
"""Alias for run_stdio."""
|
|
427
|
+
self.run_stdio()
|
|
428
|
+
|
|
429
|
+
def get_claude_config(self, script_path: Optional[str] = None) -> Dict[str, Any]:
|
|
430
|
+
"""Generate the JSON snippet for claude_desktop_config.json."""
|
|
431
|
+
target_script = script_path or os.path.abspath(sys.argv[0])
|
|
432
|
+
return {
|
|
433
|
+
"mcpServers": {
|
|
434
|
+
self.name: {
|
|
435
|
+
"command": "python",
|
|
436
|
+
"args": [target_script]
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
def print_claude_config(self, script_path: Optional[str] = None):
|
|
442
|
+
"""Print formatted JSON for Claude Desktop configuration."""
|
|
443
|
+
cfg = self.get_claude_config(script_path)
|
|
444
|
+
print(json.dumps(cfg, indent=2))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: py-easy-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Ultra-lightweight, zero-dependency Python framework to build Model Context Protocol (MCP) servers for Claude.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lui01212/easy-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/lui01212/easy-mcp.git
|
|
7
|
+
Project-URL: Issues, https://github.com/lui01212/easy-mcp/issues
|
|
8
|
+
Author-email: lui01212 <luynbv@glotechvn.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,anthropic,claude,json-rpc,llm,mcp,model-context-protocol,tools
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# py-easy-mcp ⚡🤖
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/py-easy-mcp/)
|
|
31
|
+
[](https://pypi.org/project/py-easy-mcp/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://github.com/lui01212/easy-mcp/actions)
|
|
34
|
+
[](https://github.com/lui01212/easy-mcp/issues?q=is%3Aissue+state%3Aopen+label%3A%22good+first+issue%22)
|
|
35
|
+
[](https://hacktoberfest.com/)
|
|
36
|
+
|
|
37
|
+
**Ultra-lightweight, zero-dependency Python framework for Model Context Protocol (MCP). Build MCP servers for Claude in seconds.**
|
|
38
|
+
|
|
39
|
+
Turn standard Python functions into Model Context Protocol tools, resources, and prompt templates for **Claude Desktop** and **Claude Code** with simple decorators.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ⚡ Why easy-mcp?
|
|
44
|
+
|
|
45
|
+
- **Zero dependencies:** Written in 100% pure standard Python. No heavy ASGI servers, no complex async overhead.
|
|
46
|
+
- **Full MCP Protocol Support (v0.2.0):** Supports Tools (`@mcp.tool()`), Resources (`@mcp.resource()`), and Prompt Templates (`@mcp.prompt()`).
|
|
47
|
+
- **Pythonic & Simple:** Turn any Python function into an MCP primitive with clean decorators.
|
|
48
|
+
- **Automatic Schema Inspection:** Docstrings and type annotations are automatically converted into standard JSON Schemas.
|
|
49
|
+
- **Claude Desktop Ready:** Built-in config generator outputs the exact JSON configuration to paste into `claude_desktop_config.json`.
|
|
50
|
+
- **Offline & Safe:** Standard I/O (stdio) JSON-RPC 2.0 communication.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📦 Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install py-easy-mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🚀 30-Second Quickstart
|
|
63
|
+
|
|
64
|
+
### 1. Write your server (`my_server.py`)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from easy_mcp import EasyMCP
|
|
68
|
+
|
|
69
|
+
mcp = EasyMCP(name="my-server", version="0.2.0")
|
|
70
|
+
|
|
71
|
+
# 1. Register a Tool
|
|
72
|
+
@mcp.tool()
|
|
73
|
+
def add(a: float, b: float) -> float:
|
|
74
|
+
"""Add two numbers together.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
a: First number
|
|
78
|
+
b: Second number
|
|
79
|
+
"""
|
|
80
|
+
return a + b
|
|
81
|
+
|
|
82
|
+
# 2. Register a Resource (readable context)
|
|
83
|
+
@mcp.resource("memo://project-guidelines")
|
|
84
|
+
def guidelines() -> str:
|
|
85
|
+
"""Project engineering principles."""
|
|
86
|
+
return "1. Keep it simple.\n2. Zero external dependencies.\n3. Write clear tests."
|
|
87
|
+
|
|
88
|
+
# 3. Register a Prompt Template
|
|
89
|
+
@mcp.prompt()
|
|
90
|
+
def code_review(code: str, language: str = "python") -> str:
|
|
91
|
+
"""Review code for bugs and maintainability."""
|
|
92
|
+
return f"Please review this {language} snippet:\n\n{code}"
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
mcp.run()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Add to Claude Desktop
|
|
99
|
+
|
|
100
|
+
Generate the configuration snippet:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
easy-mcp config my_server.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Output:
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"my-server": {
|
|
111
|
+
"command": "python",
|
|
112
|
+
"args": [
|
|
113
|
+
"/path/to/my_server.py"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Paste this into your `claude_desktop_config.json`:
|
|
121
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
122
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
123
|
+
|
|
124
|
+
Restart Claude Desktop, and your tools, resources, and prompts will appear!
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🛠️ CLI Utilities
|
|
129
|
+
|
|
130
|
+
Scaffold a new server in 1 second:
|
|
131
|
+
```bash
|
|
132
|
+
easy-mcp init weather-server
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Generate config for Claude Desktop:
|
|
136
|
+
```bash
|
|
137
|
+
easy-mcp config server.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 📂 Community MCP Examples (Help Wanted!)
|
|
143
|
+
|
|
144
|
+
We are building a curated collection of lightweight MCP tool servers in `examples/`:
|
|
145
|
+
- `examples/basic_math.py` - Math calculator
|
|
146
|
+
- `examples/pii_shield_mcp.py` - AI data privacy sanitizer
|
|
147
|
+
|
|
148
|
+
### Want to contribute?
|
|
149
|
+
Help us add more examples for Hacktoberfest:
|
|
150
|
+
- 🌐 SQLite database query tool
|
|
151
|
+
- 📰 Hacker News API reader
|
|
152
|
+
- 📁 Local markdown note searcher
|
|
153
|
+
- ⛅ Weather lookup
|
|
154
|
+
|
|
155
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) to submit an example!
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 📄 License
|
|
160
|
+
|
|
161
|
+
[MIT License](LICENSE) © 2026 lui01212
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
easy_mcp/__init__.py,sha256=bvChmmQxYG5S22OiNR0-bxoQ-kBEiPasKYJTELjdHhg,675
|
|
2
|
+
easy_mcp/cli.py,sha256=3wmh8ctsIoBM_HFFwFZ3OlZQwOCaeUK_9xXwEIVV9hA,3956
|
|
3
|
+
easy_mcp/protocol.py,sha256=P7x7oiCxxg-yppg6Qj3z7JU2JlP-84QhhM0imVKWR4Y,2521
|
|
4
|
+
easy_mcp/schema.py,sha256=dwzxMzV-rnqTJ2qPt8xve1PUzQNQuLJUNXR5YBBBr_c,4583
|
|
5
|
+
easy_mcp/server.py,sha256=wmanDyfy7eEe09Rqi9ytiiLfrLc_LVOpUGBCio_I9j4,14810
|
|
6
|
+
py_easy_mcp-0.2.0.dist-info/METADATA,sha256=qtdEhlsv8wPz7gxNBfgyppxRjTw16iu6sA8mqKhE2Ls,5314
|
|
7
|
+
py_easy_mcp-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
py_easy_mcp-0.2.0.dist-info/entry_points.txt,sha256=pmY6wFwySLtIsCCY3SkEs2EMXPq-AnyIoQE0sfksJQM,79
|
|
9
|
+
py_easy_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=RWxza2uvDjCv6Rh2BuAIA6Y_trYT1WF7EZ5fUFYldWc,1065
|
|
10
|
+
py_easy_mcp-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lui01212
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|