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.
@@ -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
+ ]
@@ -0,0 +1,8 @@
1
+ """
2
+ Entry point for running `python -m mcp_agent_kit`.
3
+ """
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
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