mcp-agent-kit 0.1.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.
- mcp_agent_kit/__init__.py +70 -0
- mcp_agent_kit/__main__.py +8 -0
- mcp_agent_kit/a2a.py +271 -0
- mcp_agent_kit/cli.py +480 -0
- mcp_agent_kit/client.py +365 -0
- mcp_agent_kit/errors.py +95 -0
- mcp_agent_kit/prompts.py +96 -0
- mcp_agent_kit/protocol.py +164 -0
- mcp_agent_kit/resources.py +68 -0
- mcp_agent_kit/server.py +364 -0
- mcp_agent_kit/skill.py +178 -0
- mcp_agent_kit/tools.py +250 -0
- mcp_agent_kit/validator.py +225 -0
- mcp_agent_kit-0.1.0.dist-info/METADATA +531 -0
- mcp_agent_kit-0.1.0.dist-info/RECORD +20 -0
- mcp_agent_kit-0.1.0.dist-info/WHEEL +5 -0
- mcp_agent_kit-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_agent_kit-0.1.0.dist-info/top_level.txt +2 -0
- tools/__init__.py +6 -0
- tools/example.py +67 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp-agent-kit: Production-ready MCP server scaffold with A2A support and Agent Skill packaging.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .a2a import generate_agent_card, map_mcp_tools_to_a2a_skills, run_a2a_server, TaskManager
|
|
6
|
+
from .client import DualEraMCPClient
|
|
7
|
+
from .errors import (
|
|
8
|
+
HEADER_MISMATCH,
|
|
9
|
+
INTERNAL_ERROR,
|
|
10
|
+
INVALID_PARAMS,
|
|
11
|
+
INVALID_REQUEST,
|
|
12
|
+
METHOD_NOT_FOUND,
|
|
13
|
+
PARSE_ERROR,
|
|
14
|
+
UNSUPPORTED_PROTOCOL_VERSION,
|
|
15
|
+
HeaderMismatchError,
|
|
16
|
+
InternalError,
|
|
17
|
+
InvalidParamsError,
|
|
18
|
+
InvalidRequestError,
|
|
19
|
+
MCPError,
|
|
20
|
+
MethodNotFoundError,
|
|
21
|
+
ParseError,
|
|
22
|
+
UnsupportedProtocolVersionError,
|
|
23
|
+
)
|
|
24
|
+
from .prompts import PromptRegistry
|
|
25
|
+
from .protocol import (
|
|
26
|
+
MODERN_PROTOCOL_VERSION,
|
|
27
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
28
|
+
ClientInfo,
|
|
29
|
+
RequestMeta,
|
|
30
|
+
ServerInfo,
|
|
31
|
+
format_error_response,
|
|
32
|
+
format_legacy_initialize_response,
|
|
33
|
+
format_success_response,
|
|
34
|
+
validate_stateless_request,
|
|
35
|
+
)
|
|
36
|
+
from .resources import ResourceRegistry
|
|
37
|
+
from .server import MCPServer
|
|
38
|
+
from .skill import generate_skill_markdown, validate_skill_markdown
|
|
39
|
+
from .tools import InputRequired, ToolAnnotations, ToolDefinition, ToolRegistry, validate_tool_schema
|
|
40
|
+
from .validator import ComplianceValidator
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"MCPServer",
|
|
46
|
+
"DualEraMCPClient",
|
|
47
|
+
"ToolRegistry",
|
|
48
|
+
"ToolAnnotations",
|
|
49
|
+
"ToolDefinition",
|
|
50
|
+
"InputRequired",
|
|
51
|
+
"validate_tool_schema",
|
|
52
|
+
"ResourceRegistry",
|
|
53
|
+
"PromptRegistry",
|
|
54
|
+
"ComplianceValidator",
|
|
55
|
+
"TaskManager",
|
|
56
|
+
"generate_agent_card",
|
|
57
|
+
"map_mcp_tools_to_a2a_skills",
|
|
58
|
+
"run_a2a_server",
|
|
59
|
+
"generate_skill_markdown",
|
|
60
|
+
"validate_skill_markdown",
|
|
61
|
+
"MODERN_PROTOCOL_VERSION",
|
|
62
|
+
"SUPPORTED_PROTOCOL_VERSIONS",
|
|
63
|
+
"RequestMeta",
|
|
64
|
+
"ServerInfo",
|
|
65
|
+
"ClientInfo",
|
|
66
|
+
"MCPError",
|
|
67
|
+
"UnsupportedProtocolVersionError",
|
|
68
|
+
"InvalidParamsError",
|
|
69
|
+
"HeaderMismatchError",
|
|
70
|
+
]
|
mcp_agent_kit/a2a.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2A (Agent-to-Agent) Protocol implementation.
|
|
3
|
+
Includes Agent Card generation (/.well-known/agent.json),
|
|
4
|
+
tool-to-skill automated mapping, and /a2a task lifecycle endpoint.
|
|
5
|
+
Standard library only (with optional Flask adapter).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
import http.server
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class MessagePart:
|
|
21
|
+
type: str # text, file, data
|
|
22
|
+
content: Any
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
25
|
+
if self.type == "text":
|
|
26
|
+
return {"type": "text", "text": str(self.content)}
|
|
27
|
+
elif self.type == "file":
|
|
28
|
+
if isinstance(self.content, dict):
|
|
29
|
+
return {"type": "file", **self.content}
|
|
30
|
+
return {"type": "file", "uri": str(self.content)}
|
|
31
|
+
elif self.type == "data":
|
|
32
|
+
return {"type": "data", "data": self.content}
|
|
33
|
+
return {"type": self.type, "content": self.content}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MessagePart":
|
|
37
|
+
part_type = data.get("type", "text")
|
|
38
|
+
if part_type == "text":
|
|
39
|
+
return cls(type="text", content=data.get("text", ""))
|
|
40
|
+
elif part_type == "file":
|
|
41
|
+
return cls(type="file", content=data)
|
|
42
|
+
elif part_type == "data":
|
|
43
|
+
return cls(type="data", content=data.get("data", {}))
|
|
44
|
+
return cls(type=part_type, content=data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Artifact:
|
|
49
|
+
name: str
|
|
50
|
+
type: str # text, file, data
|
|
51
|
+
data: Any
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"type": self.type,
|
|
57
|
+
"data": self.data,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class A2ATask:
|
|
63
|
+
taskId: str
|
|
64
|
+
status: str # submitted -> working -> completed | failed | input_required
|
|
65
|
+
messages: List[Dict[str, Any]] = field(default_factory=list)
|
|
66
|
+
artifacts: List[Dict[str, Any]] = field(default_factory=list)
|
|
67
|
+
error: Optional[str] = None
|
|
68
|
+
created_at: float = field(default_factory=time.time)
|
|
69
|
+
updated_at: float = field(default_factory=time.time)
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
72
|
+
res: Dict[str, Any] = {
|
|
73
|
+
"taskId": self.taskId,
|
|
74
|
+
"status": self.status,
|
|
75
|
+
"messages": self.messages,
|
|
76
|
+
"artifacts": self.artifacts,
|
|
77
|
+
"created_at": self.created_at,
|
|
78
|
+
"updated_at": self.updated_at,
|
|
79
|
+
}
|
|
80
|
+
if self.error:
|
|
81
|
+
res["error"] = self.error
|
|
82
|
+
return res
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def map_mcp_tools_to_a2a_skills(tools_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
Maps MCP tools list automatically to A2A skills with matching inputModes and outputModes.
|
|
88
|
+
"""
|
|
89
|
+
skills: List[Dict[str, Any]] = []
|
|
90
|
+
for tool in tools_list:
|
|
91
|
+
skills.append(
|
|
92
|
+
{
|
|
93
|
+
"id": tool.get("name", ""),
|
|
94
|
+
"name": tool.get("name", ""),
|
|
95
|
+
"description": tool.get("description", ""),
|
|
96
|
+
"inputModes": ["data", "text"],
|
|
97
|
+
"outputModes": ["data", "text"],
|
|
98
|
+
"parameters": tool.get("inputSchema", {}),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
return skills
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def generate_agent_card(
|
|
105
|
+
name: str,
|
|
106
|
+
description: str,
|
|
107
|
+
base_url: str = "http://localhost:8000",
|
|
108
|
+
tools_list: Optional[List[Dict[str, Any]]] = None,
|
|
109
|
+
version: str = "1.0.0",
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Generates A2A Agent Card compliant with schemaVersion 1.0.
|
|
113
|
+
"""
|
|
114
|
+
tools = tools_list or []
|
|
115
|
+
skills = map_mcp_tools_to_a2a_skills(tools)
|
|
116
|
+
clean_url = base_url.rstrip("/")
|
|
117
|
+
return {
|
|
118
|
+
"schemaVersion": "1.0",
|
|
119
|
+
"name": name,
|
|
120
|
+
"description": description,
|
|
121
|
+
"url": f"{clean_url}/a2a",
|
|
122
|
+
"version": version,
|
|
123
|
+
"skills": skills,
|
|
124
|
+
"capabilities": {
|
|
125
|
+
"streaming": True,
|
|
126
|
+
"pushNotifications": False,
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TaskManager:
|
|
132
|
+
"""Manages A2A task lifecycle: submitted -> working -> completed | failed | input_required."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, task_executor: Optional[Callable[[A2ATask], None]] = None):
|
|
135
|
+
self._tasks: Dict[str, A2ATask] = {}
|
|
136
|
+
self._lock = threading.Lock()
|
|
137
|
+
self.task_executor = task_executor or self._default_executor
|
|
138
|
+
|
|
139
|
+
def create_or_resume_task(self, data: Dict[str, Any]) -> A2ATask:
|
|
140
|
+
with self._lock:
|
|
141
|
+
task_id = data.get("taskId")
|
|
142
|
+
if task_id and task_id in self._tasks:
|
|
143
|
+
task = self._tasks[task_id]
|
|
144
|
+
# Resume input_required task
|
|
145
|
+
if task.status == "input_required":
|
|
146
|
+
task.status = "working"
|
|
147
|
+
task.updated_at = time.time()
|
|
148
|
+
if "messages" in data:
|
|
149
|
+
task.messages.extend(data["messages"])
|
|
150
|
+
self.task_executor(task)
|
|
151
|
+
return task
|
|
152
|
+
|
|
153
|
+
new_id = task_id or f"task_{uuid.uuid4().hex[:12]}"
|
|
154
|
+
task = A2ATask(
|
|
155
|
+
taskId=new_id,
|
|
156
|
+
status="submitted",
|
|
157
|
+
messages=data.get("messages", []),
|
|
158
|
+
)
|
|
159
|
+
self._tasks[new_id] = task
|
|
160
|
+
|
|
161
|
+
# Execute transition: submitted -> working -> completed/input_required
|
|
162
|
+
self.task_executor(task)
|
|
163
|
+
return task
|
|
164
|
+
|
|
165
|
+
def get_task(self, task_id: str) -> Optional[A2ATask]:
|
|
166
|
+
with self._lock:
|
|
167
|
+
return self._tasks.get(task_id)
|
|
168
|
+
|
|
169
|
+
def _default_executor(self, task: A2ATask) -> None:
|
|
170
|
+
"""Default task state transition simulator."""
|
|
171
|
+
task.status = "working"
|
|
172
|
+
task.updated_at = time.time()
|
|
173
|
+
|
|
174
|
+
# Check if the latest message part requests input_required
|
|
175
|
+
if task.messages:
|
|
176
|
+
latest_msg = task.messages[-1]
|
|
177
|
+
parts = latest_msg.get("parts", [])
|
|
178
|
+
for p in parts:
|
|
179
|
+
text_content = p.get("text", "")
|
|
180
|
+
if "require_input" in text_content or p.get("require_input"):
|
|
181
|
+
task.status = "input_required"
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Complete task and produce typed artifact
|
|
185
|
+
task.status = "completed"
|
|
186
|
+
task.artifacts.append(
|
|
187
|
+
Artifact(
|
|
188
|
+
name="task_output",
|
|
189
|
+
type="data",
|
|
190
|
+
data={"status": "success", "processed_parts": len(task.messages)},
|
|
191
|
+
).to_dict()
|
|
192
|
+
)
|
|
193
|
+
task.updated_at = time.time()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class A2AHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
|
197
|
+
"""Stdlib HTTP request handler for A2A endpoints."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, *args, agent_card: Dict[str, Any], task_manager: TaskManager, **kwargs):
|
|
200
|
+
self.agent_card = agent_card
|
|
201
|
+
self.task_manager = task_manager
|
|
202
|
+
super().__init__(*args, **kwargs)
|
|
203
|
+
|
|
204
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
205
|
+
# Avoid cluttering stdout
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
def _send_json(self, status_code: int, data: Any) -> None:
|
|
209
|
+
payload = json.dumps(data).encode("utf-8")
|
|
210
|
+
self.send_response(status_code)
|
|
211
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
212
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
213
|
+
self.end_headers()
|
|
214
|
+
self.wfile.write(payload)
|
|
215
|
+
|
|
216
|
+
def do_GET(self) -> None:
|
|
217
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
218
|
+
path = parsed.path
|
|
219
|
+
|
|
220
|
+
# Agent Card endpoints
|
|
221
|
+
if path in ("/.well-known/agent.json", "/a2a/agent.json", "/agent.json"):
|
|
222
|
+
self._send_json(200, self.agent_card)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# GET tasks/{id}
|
|
226
|
+
match = re.match(r"^(?:/a2a)?/tasks/([^/]+)$", path)
|
|
227
|
+
if match:
|
|
228
|
+
task_id = match.group(1)
|
|
229
|
+
task = self.task_manager.get_task(task_id)
|
|
230
|
+
if not task:
|
|
231
|
+
self._send_json(404, {"error": f"Task '{task_id}' not found"})
|
|
232
|
+
return
|
|
233
|
+
self._send_json(200, task.to_dict())
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
self._send_json(404, {"error": f"Endpoint not found: {path}"})
|
|
237
|
+
|
|
238
|
+
def do_POST(self) -> None:
|
|
239
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
240
|
+
path = parsed.path
|
|
241
|
+
|
|
242
|
+
if path in ("/a2a/tasks/send", "/tasks/send"):
|
|
243
|
+
try:
|
|
244
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
245
|
+
body = self.rfile.read(length).decode("utf-8") if length > 0 else "{}"
|
|
246
|
+
data = json.loads(body)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
self._send_json(400, {"error": f"Invalid JSON payload: {str(exc)}"})
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
task = self.task_manager.create_or_resume_task(data)
|
|
252
|
+
self._send_json(200, {"taskId": task.taskId, "status": task.status})
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
self._send_json(404, {"error": f"Endpoint not found: {path}"})
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_a2a_server(
|
|
259
|
+
agent_card: Dict[str, Any],
|
|
260
|
+
port: int = 8000,
|
|
261
|
+
host: str = "0.0.0.0",
|
|
262
|
+
task_manager: Optional[TaskManager] = None,
|
|
263
|
+
) -> http.server.HTTPServer:
|
|
264
|
+
"""Runs A2A server on stdlib http.server with zero runtime dependencies."""
|
|
265
|
+
tm = task_manager or TaskManager()
|
|
266
|
+
|
|
267
|
+
def handler_factory(*args, **kwargs):
|
|
268
|
+
return A2AHTTPRequestHandler(*args, agent_card=agent_card, task_manager=tm, **kwargs)
|
|
269
|
+
|
|
270
|
+
server = http.server.HTTPServer((host, port), handler_factory)
|
|
271
|
+
return server
|