multi-agent-platform 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.
- cli/__init__.py +0 -0
- cli/action_item_escalation.py +177 -0
- cli/agent_client.py +554 -0
- cli/bridge_state.py +43 -0
- cli/commands/__init__.py +13 -0
- cli/commands/action.py +142 -0
- cli/commands/agent.py +117 -0
- cli/commands/audit.py +68 -0
- cli/commands/docs.py +179 -0
- cli/commands/experiment.py +755 -0
- cli/commands/feedback.py +106 -0
- cli/commands/notification.py +213 -0
- cli/commands/persona.py +63 -0
- cli/commands/project.py +87 -0
- cli/commands/runtime.py +105 -0
- cli/commands/topic.py +361 -0
- cli/e2e_collab.py +602 -0
- cli/git_checkpoint.py +68 -0
- cli/host_worker_types.py +151 -0
- cli/main.py +1553 -0
- cli/map_command_client.py +497 -0
- cli/participant_worker.py +255 -0
- cli/reviewer_worker.py +263 -0
- cli/runtime/__init__.py +5 -0
- cli/runtime/run_lock.py +497 -0
- cli/runtime_chat.py +317 -0
- cli/session_wake_log.py +235 -0
- cli/simple_waker.py +950 -0
- cli/table_render.py +113 -0
- cli/wake_backend.py +236 -0
- cli/worker_cycle_log.py +36 -0
- map_client/__init__.py +37 -0
- map_client/bootstrap.py +193 -0
- map_client/client.py +1045 -0
- map_client/config.py +21 -0
- map_client/errors.py +283 -0
- map_client/exceptions.py +130 -0
- map_client/plan_evidence.py +159 -0
- map_client/project_config.py +153 -0
- map_client/result_template.py +167 -0
- map_client/testing.py +27 -0
- map_mcp/__init__.py +4 -0
- map_mcp/_utils.py +28 -0
- map_mcp/auth.py +34 -0
- map_mcp/config.py +50 -0
- map_mcp/context.py +39 -0
- map_mcp/main.py +75 -0
- map_mcp/server.py +573 -0
- map_mcp/session.py +79 -0
- map_sdk/__init__.py +29 -0
- map_sdk/evidence.py +68 -0
- map_types/__init__.py +203 -0
- map_types/enums.py +199 -0
- map_types/schemas.py +1351 -0
- multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
- multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
- multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
- multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
- multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
- multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
- server/__init__.py +0 -0
- server/__version__.py +14 -0
- server/api/__init__.py +0 -0
- server/api/action_items.py +138 -0
- server/api/agents.py +412 -0
- server/api/audit.py +54 -0
- server/api/background_tasks.py +18 -0
- server/api/common.py +117 -0
- server/api/deps.py +30 -0
- server/api/experiments.py +858 -0
- server/api/feedback.py +75 -0
- server/api/notifications.py +22 -0
- server/api/projects.py +209 -0
- server/api/router.py +25 -0
- server/api/status.py +33 -0
- server/api/topics.py +302 -0
- server/api/webhooks.py +74 -0
- server/auth/__init__.py +8 -0
- server/auth/experiment_access.py +66 -0
- server/config.py +38 -0
- server/db/__init__.py +3 -0
- server/db/base.py +5 -0
- server/db/deadlock_retry.py +146 -0
- server/db/session.py +41 -0
- server/domain/__init__.py +3 -0
- server/domain/encrypted_types.py +63 -0
- server/domain/models.py +713 -0
- server/domain/schemas.py +3 -0
- server/domain/state_machine.py +79 -0
- server/domain/topic_ack_constants.py +9 -0
- server/main.py +148 -0
- server/scripts/__init__.py +0 -0
- server/scripts/migrate_notification_unique.py +231 -0
- server/scripts/purge_audit_pollution.py +116 -0
- server/services/__init__.py +0 -0
- server/services/_lookups.py +26 -0
- server/services/acceptance_service.py +90 -0
- server/services/action_item_migration_service.py +190 -0
- server/services/action_item_service.py +200 -0
- server/services/agent_work_service.py +405 -0
- server/services/archive_lint_service.py +156 -0
- server/services/audit_service.py +457 -0
- server/services/auth.py +66 -0
- server/services/comment_service.py +173 -0
- server/services/errors.py +65 -0
- server/services/escalation_resolver.py +248 -0
- server/services/evidence_service.py +88 -0
- server/services/experiment_capabilities_service.py +277 -0
- server/services/inbound_event_service.py +111 -0
- server/services/lock_service.py +273 -0
- server/services/log_service.py +202 -0
- server/services/mention_service.py +730 -0
- server/services/notification_service.py +939 -0
- server/services/notification_stream.py +138 -0
- server/services/permissions.py +147 -0
- server/services/persona_activity_service.py +108 -0
- server/services/phase_owner_resolver.py +95 -0
- server/services/phase_service.py +381 -0
- server/services/plan_marker_service.py +235 -0
- server/services/plan_service.py +186 -0
- server/services/platform_feedback_service.py +114 -0
- server/services/project_service.py +534 -0
- server/services/project_status_service.py +132 -0
- server/services/review_service.py +707 -0
- server/services/secret_encryption.py +97 -0
- server/services/similarity_service.py +119 -0
- server/services/sse_event_schemas.py +17 -0
- server/services/status_service.py +68 -0
- server/services/template_service.py +134 -0
- server/services/text_utils.py +19 -0
- server/services/thread_activity.py +180 -0
- server/services/todo_persona_filter.py +73 -0
- server/services/todo_service.py +604 -0
- server/services/topic_ack_service.py +312 -0
- server/services/topic_action_item_ops.py +538 -0
- server/services/topic_comment_kind.py +14 -0
- server/services/topic_comment_service.py +237 -0
- server/services/topic_helpers.py +32 -0
- server/services/topic_lifecycle_service.py +478 -0
- server/services/topic_progress_service.py +40 -0
- server/services/topic_resolve_service.py +234 -0
- server/services/topic_service.py +102 -0
- server/services/topic_work_item_service.py +570 -0
- server/services/webhook_service.py +273 -0
map_mcp/server.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from map_client.client import MAPClient
|
|
7
|
+
from map_client.config import load_config
|
|
8
|
+
from map_types import (
|
|
9
|
+
CommentAnchorType,
|
|
10
|
+
CommentCreate,
|
|
11
|
+
ExperimentComplete,
|
|
12
|
+
ExperimentCreate,
|
|
13
|
+
ExperimentLogCreate,
|
|
14
|
+
ExperimentPhase,
|
|
15
|
+
ExperimentResultDecision,
|
|
16
|
+
PlanInput,
|
|
17
|
+
PlanRevise,
|
|
18
|
+
ProjectStatusRevise,
|
|
19
|
+
ReviewCreate,
|
|
20
|
+
ReviewItemStatus,
|
|
21
|
+
TopicCommentCreate,
|
|
22
|
+
TopicCreate,
|
|
23
|
+
TopicStatus,
|
|
24
|
+
)
|
|
25
|
+
from mcp.server.fastmcp import FastMCP
|
|
26
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
27
|
+
from starlette.requests import Request
|
|
28
|
+
from starlette.responses import JSONResponse
|
|
29
|
+
|
|
30
|
+
from map_mcp._utils import dump, dumps_json, parse_uuid
|
|
31
|
+
from map_mcp.auth import BearerTokenMiddleware
|
|
32
|
+
from map_mcp.session import ClientResolver, token_param
|
|
33
|
+
|
|
34
|
+
Token = Annotated[str | None, token_param()]
|
|
35
|
+
|
|
36
|
+
_INSTRUCTIONS = """\
|
|
37
|
+
Multi-Agent Platform (MAP) — experiment collaboration for AI agents.
|
|
38
|
+
|
|
39
|
+
Authentication (transport layer):
|
|
40
|
+
- HTTP: set Authorization: Bearer <MAP agent token> on the MCP connection (e.g. Cursor mcp.json headers).
|
|
41
|
+
- stdio: set MAP_TOKEN in the MCP server process environment.
|
|
42
|
+
- Use separate MCP entries (map-admin / map-agent) for admin vs project-bound agent roles.
|
|
43
|
+
|
|
44
|
+
Start with get_me to confirm role and bound project_key, then read project context via get_project_status.
|
|
45
|
+
|
|
46
|
+
Typical workflow:
|
|
47
|
+
1. get_project_status for project context
|
|
48
|
+
2. create_experiment with a plan (optionally submit_for_review; topic_id only if you are the topic host)
|
|
49
|
+
3. create_review with reasonable and unreasonable items (often a second MCP connection / reviewer token)
|
|
50
|
+
4. revise_plan or create_comment to address disputes; update_review_item to resolve items
|
|
51
|
+
5. approve_experiment → start_experiment → complete_experiment to submit results for review
|
|
52
|
+
6. a different reviewer/admin calls accept_experiment_result or reject_experiment_result
|
|
53
|
+
|
|
54
|
+
Admin-only tools (create_project, list_projects, etc.) require an admin token on the connection.
|
|
55
|
+
|
|
56
|
+
Read-only experiment context: map://experiment/{experiment_id}
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_server(
|
|
61
|
+
client: MAPClient | None = None,
|
|
62
|
+
*,
|
|
63
|
+
api_url: str | None = None,
|
|
64
|
+
transport: httpx.BaseTransport | None = None,
|
|
65
|
+
host: str = "127.0.0.1",
|
|
66
|
+
port: int = 8080,
|
|
67
|
+
path: str = "/mcp",
|
|
68
|
+
) -> FastMCP:
|
|
69
|
+
"""Build a FastMCP server. HTTP auth via Authorization Bearer; stdio may use a default client."""
|
|
70
|
+
resolved_api_url = api_url or (client.base_url if client is not None else load_config()["api_url"])
|
|
71
|
+
resolved_transport = transport or (client._transport if client is not None else None)
|
|
72
|
+
resolver = ClientResolver(resolved_api_url, client, transport=resolved_transport)
|
|
73
|
+
|
|
74
|
+
transport_security: TransportSecuritySettings | None = None
|
|
75
|
+
if host in ("127.0.0.1", "localhost", "::1"):
|
|
76
|
+
transport_security = TransportSecuritySettings(
|
|
77
|
+
enable_dns_rebinding_protection=True,
|
|
78
|
+
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
|
|
79
|
+
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
mcp = FastMCP(
|
|
83
|
+
"Multi-Agent Platform",
|
|
84
|
+
instructions=_INSTRUCTIONS,
|
|
85
|
+
host=host,
|
|
86
|
+
port=port,
|
|
87
|
+
streamable_http_path=path,
|
|
88
|
+
transport_security=transport_security,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@mcp.custom_route("/health", methods=["GET"], name="health")
|
|
92
|
+
async def health(_: Request) -> JSONResponse:
|
|
93
|
+
return JSONResponse({"status": "ok", "service": "map-mcp", "auth": "bearer"})
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
def get_todos(token: Token = None) -> dict[str, Any]:
|
|
97
|
+
"""Return the current agent's to-do list: pending reviews, pending replies, own open experiments and topics."""
|
|
98
|
+
with resolver.use(token) as (c, _ctx):
|
|
99
|
+
return dump(c.get_todos())
|
|
100
|
+
|
|
101
|
+
@mcp.tool()
|
|
102
|
+
def list_notifications(
|
|
103
|
+
unread_only: bool = False,
|
|
104
|
+
category: str | None = None,
|
|
105
|
+
target_type: str | None = None,
|
|
106
|
+
limit: int = 50,
|
|
107
|
+
offset: int = 0,
|
|
108
|
+
token: Token = None,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""List in-app notifications for the current agent (paginated, optional unread filter)."""
|
|
111
|
+
with resolver.use(token) as (c, _ctx):
|
|
112
|
+
return dump(
|
|
113
|
+
c.list_notifications(
|
|
114
|
+
unread_only=unread_only,
|
|
115
|
+
category=category,
|
|
116
|
+
target_type=target_type,
|
|
117
|
+
limit=limit,
|
|
118
|
+
offset=offset,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@mcp.tool()
|
|
123
|
+
def mark_notification_read(notification_id: str, token: Token = None) -> dict[str, Any]:
|
|
124
|
+
"""Mark a single in-app notification as read."""
|
|
125
|
+
with resolver.use(token) as (c, _ctx):
|
|
126
|
+
return dump(c.mark_notification_read(parse_uuid(notification_id, "notification_id")))
|
|
127
|
+
|
|
128
|
+
@mcp.tool()
|
|
129
|
+
def mark_all_notifications_read(token: Token = None) -> dict[str, Any]:
|
|
130
|
+
"""Mark all in-app notifications as read for the current agent."""
|
|
131
|
+
with resolver.use(token) as (c, _ctx):
|
|
132
|
+
return dump(c.mark_all_notifications_read())
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
def get_audit_history(target_type: str, target_id: str, token: Token = None) -> list[dict[str, Any]]:
|
|
136
|
+
"""Return the audit trail for a given object (e.g. target_type='experiment', 'topic')."""
|
|
137
|
+
with resolver.use(token) as (c, _ctx):
|
|
138
|
+
return dump(c.list_audit_for_target(target_type, parse_uuid(target_id, "target_id")))
|
|
139
|
+
|
|
140
|
+
@mcp.tool()
|
|
141
|
+
def get_me(token: Token = None) -> dict[str, Any]:
|
|
142
|
+
"""Return the authenticated agent profile (role, project_id, project_key)."""
|
|
143
|
+
with resolver.use(token) as (c, _ctx):
|
|
144
|
+
return dump(c.get_me())
|
|
145
|
+
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
def get_project_status(project_id: str | None = None, token: Token = None) -> dict[str, Any]:
|
|
148
|
+
"""Get project status: structured snapshot (active_experiments, open_topics, experiment_counts_by_phase, etc.) plus narrative status_md. Prefer snapshot fields for experiment/topic lists; use status_md for goals, blockers, and next steps only."""
|
|
149
|
+
with resolver.use(token) as (c, ctx):
|
|
150
|
+
pid = ctx.resolve_project_id(project_id)
|
|
151
|
+
return dump(c.get_project_status(pid))
|
|
152
|
+
|
|
153
|
+
@mcp.tool()
|
|
154
|
+
def list_project_status_versions(project_id: str | None = None, token: Token = None) -> list[dict[str, Any]]:
|
|
155
|
+
"""List Current Status markdown revisions for a project (newest first)."""
|
|
156
|
+
with resolver.use(token) as (c, ctx):
|
|
157
|
+
pid = ctx.resolve_project_id(project_id)
|
|
158
|
+
return dump(c.list_project_status_versions(pid))
|
|
159
|
+
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
def get_project_status_version(
|
|
162
|
+
project_id: str | None,
|
|
163
|
+
version: int,
|
|
164
|
+
token: Token = None,
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Get a specific Current Status markdown revision by version number."""
|
|
167
|
+
with resolver.use(token) as (c, ctx):
|
|
168
|
+
pid = ctx.resolve_project_id(project_id)
|
|
169
|
+
return dump(c.get_project_status_version(pid, version))
|
|
170
|
+
|
|
171
|
+
@mcp.tool()
|
|
172
|
+
def list_experiments(
|
|
173
|
+
project_id: str | None = None,
|
|
174
|
+
phase: str | None = None,
|
|
175
|
+
creator_agent_id: str | None = None,
|
|
176
|
+
q: str | None = None,
|
|
177
|
+
page: int = 1,
|
|
178
|
+
page_size: int = 100,
|
|
179
|
+
include_archived: bool = False,
|
|
180
|
+
token: Token = None,
|
|
181
|
+
) -> list[dict[str, Any]]:
|
|
182
|
+
"""List experiments in a project, with optional phase/search/pagination/archive filters."""
|
|
183
|
+
with resolver.use(token) as (c, ctx):
|
|
184
|
+
pid = ctx.resolve_project_id(project_id)
|
|
185
|
+
phase_enum = ExperimentPhase(phase) if phase else None
|
|
186
|
+
creator_id = parse_uuid(creator_agent_id, "creator_agent_id") if creator_agent_id else None
|
|
187
|
+
return dump(
|
|
188
|
+
c.list_experiments(
|
|
189
|
+
pid,
|
|
190
|
+
phase=phase_enum,
|
|
191
|
+
creator_agent_id=creator_id,
|
|
192
|
+
q=q,
|
|
193
|
+
page=page,
|
|
194
|
+
page_size=page_size,
|
|
195
|
+
include_archived=include_archived,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@mcp.tool()
|
|
200
|
+
def list_topics(
|
|
201
|
+
project_id: str | None = None,
|
|
202
|
+
status: str | None = None,
|
|
203
|
+
creator_agent_id: str | None = None,
|
|
204
|
+
q: str | None = None,
|
|
205
|
+
page: int = 1,
|
|
206
|
+
page_size: int = 100,
|
|
207
|
+
include_archived: bool = False,
|
|
208
|
+
token: Token = None,
|
|
209
|
+
) -> list[dict[str, Any]]:
|
|
210
|
+
"""List discussion topics in a project, with optional status/search/pagination/archive filters."""
|
|
211
|
+
with resolver.use(token) as (c, ctx):
|
|
212
|
+
pid = ctx.resolve_project_id(project_id)
|
|
213
|
+
st = TopicStatus(status) if status else None
|
|
214
|
+
creator_id = parse_uuid(creator_agent_id, "creator_agent_id") if creator_agent_id else None
|
|
215
|
+
return dump(
|
|
216
|
+
c.list_topics(
|
|
217
|
+
pid,
|
|
218
|
+
status=st,
|
|
219
|
+
creator_agent_id=creator_id,
|
|
220
|
+
q=q,
|
|
221
|
+
page=page,
|
|
222
|
+
page_size=page_size,
|
|
223
|
+
include_archived=include_archived,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@mcp.tool()
|
|
228
|
+
def get_topic(topic_id: str, token: Token = None) -> dict[str, Any]:
|
|
229
|
+
"""Get a topic detail: discussion thread, linked experiments, and counts."""
|
|
230
|
+
with resolver.use(token) as (c, _ctx):
|
|
231
|
+
return dump(c.get_topic(parse_uuid(topic_id, "topic_id")))
|
|
232
|
+
|
|
233
|
+
@mcp.tool()
|
|
234
|
+
def create_topic(
|
|
235
|
+
title: str,
|
|
236
|
+
project_id: str | None = None,
|
|
237
|
+
description: str | None = None,
|
|
238
|
+
token: Token = None,
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
"""Create a lightweight discussion topic (no plan required) in the bound or specified project."""
|
|
241
|
+
with resolver.use(token) as (c, ctx):
|
|
242
|
+
pid = ctx.resolve_project_id(project_id)
|
|
243
|
+
payload = TopicCreate(title=title, description=description)
|
|
244
|
+
return dump(c.create_topic(pid, payload))
|
|
245
|
+
|
|
246
|
+
@mcp.tool()
|
|
247
|
+
def create_topic_comment(
|
|
248
|
+
topic_id: str,
|
|
249
|
+
body: str,
|
|
250
|
+
parent_id: str | None = None,
|
|
251
|
+
token: Token = None,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
"""Add a comment to a topic discussion thread."""
|
|
254
|
+
with resolver.use(token) as (c, _ctx):
|
|
255
|
+
payload = TopicCommentCreate(
|
|
256
|
+
body=body,
|
|
257
|
+
parent_id=parse_uuid(parent_id, "parent_id") if parent_id else None,
|
|
258
|
+
)
|
|
259
|
+
return dump(c.create_topic_comment(parse_uuid(topic_id, "topic_id"), payload))
|
|
260
|
+
|
|
261
|
+
@mcp.tool()
|
|
262
|
+
def close_topic(topic_id: str, token: Token = None) -> dict[str, Any]:
|
|
263
|
+
"""Close a topic (open → closed)."""
|
|
264
|
+
with resolver.use(token) as (c, _ctx):
|
|
265
|
+
return dump(c.close_topic(parse_uuid(topic_id, "topic_id")))
|
|
266
|
+
|
|
267
|
+
@mcp.tool()
|
|
268
|
+
def reopen_topic(topic_id: str, token: Token = None) -> dict[str, Any]:
|
|
269
|
+
"""Reopen a closed topic (closed → open)."""
|
|
270
|
+
with resolver.use(token) as (c, _ctx):
|
|
271
|
+
return dump(c.reopen_topic(parse_uuid(topic_id, "topic_id")))
|
|
272
|
+
|
|
273
|
+
@mcp.tool()
|
|
274
|
+
def get_experiment(experiment_id: str, token: Token = None) -> dict[str, Any]:
|
|
275
|
+
"""Get experiment details including current plan summary."""
|
|
276
|
+
with resolver.use(token) as (c, _ctx):
|
|
277
|
+
return dump(c.get_experiment(parse_uuid(experiment_id, "experiment_id")))
|
|
278
|
+
|
|
279
|
+
@mcp.tool()
|
|
280
|
+
def create_experiment(
|
|
281
|
+
title: str,
|
|
282
|
+
plan_content_md: str,
|
|
283
|
+
project_id: str | None = None,
|
|
284
|
+
description: str | None = None,
|
|
285
|
+
submit_for_review: bool = False,
|
|
286
|
+
topic_id: str | None = None,
|
|
287
|
+
token: Token = None,
|
|
288
|
+
) -> dict[str, Any]:
|
|
289
|
+
"""Create an experiment with an initial plan. When topic_id is set, only the topic host (or admin) may call this."""
|
|
290
|
+
with resolver.use(token) as (c, ctx):
|
|
291
|
+
pid = ctx.resolve_project_id(project_id)
|
|
292
|
+
payload = ExperimentCreate(
|
|
293
|
+
title=title,
|
|
294
|
+
description=description,
|
|
295
|
+
plan=PlanInput(content_md=plan_content_md),
|
|
296
|
+
submit_for_review=submit_for_review,
|
|
297
|
+
topic_id=parse_uuid(topic_id, "topic_id") if topic_id else None,
|
|
298
|
+
)
|
|
299
|
+
return dump(c.create_experiment(pid, payload))
|
|
300
|
+
|
|
301
|
+
@mcp.tool()
|
|
302
|
+
def submit_for_review(experiment_id: str, token: Token = None) -> dict[str, Any]:
|
|
303
|
+
"""Move experiment from draft to review phase."""
|
|
304
|
+
with resolver.use(token) as (c, _ctx):
|
|
305
|
+
return dump(c.submit_for_review(parse_uuid(experiment_id, "experiment_id")))
|
|
306
|
+
|
|
307
|
+
@mcp.tool()
|
|
308
|
+
def approve_experiment(experiment_id: str, token: Token = None) -> dict[str, Any]:
|
|
309
|
+
"""Approve experiment when all unreasonable review items are resolved."""
|
|
310
|
+
with resolver.use(token) as (c, _ctx):
|
|
311
|
+
return dump(c.approve_experiment(parse_uuid(experiment_id, "experiment_id")))
|
|
312
|
+
|
|
313
|
+
@mcp.tool()
|
|
314
|
+
def withdraw_from_review(experiment_id: str, token: Token = None) -> dict[str, Any]:
|
|
315
|
+
"""Withdraw experiment from review back to draft."""
|
|
316
|
+
with resolver.use(token) as (c, _ctx):
|
|
317
|
+
return dump(c.withdraw_from_review(parse_uuid(experiment_id, "experiment_id")))
|
|
318
|
+
|
|
319
|
+
@mcp.tool()
|
|
320
|
+
def cancel_experiment(experiment_id: str, token: Token = None) -> dict[str, Any]:
|
|
321
|
+
"""Cancel an experiment."""
|
|
322
|
+
with resolver.use(token) as (c, _ctx):
|
|
323
|
+
return dump(c.cancel_experiment(parse_uuid(experiment_id, "experiment_id")))
|
|
324
|
+
|
|
325
|
+
@mcp.tool()
|
|
326
|
+
def start_experiment(experiment_id: str, token: Token = None) -> dict[str, Any]:
|
|
327
|
+
"""Start an approved experiment (approved → running)."""
|
|
328
|
+
with resolver.use(token) as (c, _ctx):
|
|
329
|
+
return dump(c.start_experiment(parse_uuid(experiment_id, "experiment_id")))
|
|
330
|
+
|
|
331
|
+
@mcp.tool()
|
|
332
|
+
def complete_experiment(
|
|
333
|
+
experiment_id: str,
|
|
334
|
+
summary: str,
|
|
335
|
+
content_md: str,
|
|
336
|
+
metadata: dict[str, Any] | None = None,
|
|
337
|
+
token: Token = None,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
"""Submit running experiment results for review; metadata must include deployment/test evidence."""
|
|
340
|
+
with resolver.use(token) as (c, _ctx):
|
|
341
|
+
payload = ExperimentComplete(summary=summary, content_md=content_md, metadata=metadata)
|
|
342
|
+
return dump(c.complete_experiment(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
343
|
+
|
|
344
|
+
@mcp.tool()
|
|
345
|
+
def accept_experiment_result(
|
|
346
|
+
experiment_id: str,
|
|
347
|
+
summary: str,
|
|
348
|
+
content_md: str,
|
|
349
|
+
metadata: dict[str, Any] | None = None,
|
|
350
|
+
token: Token = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
"""Approve submitted experiment results (result_review → done). Must be a different agent or admin."""
|
|
353
|
+
with resolver.use(token) as (c, _ctx):
|
|
354
|
+
payload = ExperimentResultDecision(summary=summary, content_md=content_md, metadata=metadata)
|
|
355
|
+
return dump(c.accept_experiment_result(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
356
|
+
|
|
357
|
+
@mcp.tool()
|
|
358
|
+
def reject_experiment_result(
|
|
359
|
+
experiment_id: str,
|
|
360
|
+
summary: str,
|
|
361
|
+
content_md: str,
|
|
362
|
+
metadata: dict[str, Any] | None = None,
|
|
363
|
+
token: Token = None,
|
|
364
|
+
) -> dict[str, Any]:
|
|
365
|
+
"""Reject submitted experiment results and return the experiment to running for rework."""
|
|
366
|
+
with resolver.use(token) as (c, _ctx):
|
|
367
|
+
payload = ExperimentResultDecision(summary=summary, content_md=content_md, metadata=metadata)
|
|
368
|
+
return dump(c.reject_experiment_result(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
369
|
+
|
|
370
|
+
@mcp.tool()
|
|
371
|
+
def list_plans(experiment_id: str, token: Token = None) -> list[dict[str, Any]]:
|
|
372
|
+
"""List all plan versions for an experiment."""
|
|
373
|
+
with resolver.use(token) as (c, _ctx):
|
|
374
|
+
return dump(c.list_plans(parse_uuid(experiment_id, "experiment_id")))
|
|
375
|
+
|
|
376
|
+
@mcp.tool()
|
|
377
|
+
def get_plan(experiment_id: str, version: int, token: Token = None) -> dict[str, Any]:
|
|
378
|
+
"""Get a specific plan version by number."""
|
|
379
|
+
with resolver.use(token) as (c, _ctx):
|
|
380
|
+
return dump(c.get_plan(parse_uuid(experiment_id, "experiment_id"), version))
|
|
381
|
+
|
|
382
|
+
@mcp.tool()
|
|
383
|
+
def revise_plan(
|
|
384
|
+
experiment_id: str,
|
|
385
|
+
content_md: str,
|
|
386
|
+
change_note: str | None = None,
|
|
387
|
+
addressed_item_ids: list[str] | None = None,
|
|
388
|
+
token: Token = None,
|
|
389
|
+
) -> dict[str, Any]:
|
|
390
|
+
"""Create a new plan revision; optionally link addressed unreasonable review items."""
|
|
391
|
+
with resolver.use(token) as (c, _ctx):
|
|
392
|
+
item_ids = [parse_uuid(item_id, "addressed_item_id") for item_id in (addressed_item_ids or [])]
|
|
393
|
+
payload = PlanRevise(content_md=content_md, change_note=change_note, addressed_item_ids=item_ids)
|
|
394
|
+
return dump(c.revise_plan(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
def create_review(
|
|
398
|
+
experiment_id: str,
|
|
399
|
+
reasonable_items: list[str] | None = None,
|
|
400
|
+
unreasonable_items: list[str] | None = None,
|
|
401
|
+
token: Token = None,
|
|
402
|
+
) -> dict[str, Any]:
|
|
403
|
+
"""Submit a structured review with reasonable and unreasonable items."""
|
|
404
|
+
with resolver.use(token) as (c, _ctx):
|
|
405
|
+
payload = ReviewCreate(
|
|
406
|
+
reasonable_items=reasonable_items or [],
|
|
407
|
+
unreasonable_items=unreasonable_items or [],
|
|
408
|
+
)
|
|
409
|
+
return dump(c.create_review(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
410
|
+
|
|
411
|
+
@mcp.tool()
|
|
412
|
+
def list_reviews(experiment_id: str, token: Token = None) -> list[dict[str, Any]]:
|
|
413
|
+
"""List all reviews on an experiment."""
|
|
414
|
+
with resolver.use(token) as (c, _ctx):
|
|
415
|
+
return dump(c.list_reviews(parse_uuid(experiment_id, "experiment_id")))
|
|
416
|
+
|
|
417
|
+
@mcp.tool()
|
|
418
|
+
def update_review_item(item_id: str, status: str, token: Token = None) -> dict[str, Any]:
|
|
419
|
+
"""Update a review item status (open, addressed, rebutted, resolved, withdrawn, escalated)."""
|
|
420
|
+
with resolver.use(token) as (c, _ctx):
|
|
421
|
+
return dump(
|
|
422
|
+
c.update_review_item(
|
|
423
|
+
parse_uuid(item_id, "item_id"),
|
|
424
|
+
ReviewItemStatus(status),
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@mcp.tool()
|
|
429
|
+
def create_comment(
|
|
430
|
+
experiment_id: str,
|
|
431
|
+
anchor_type: str,
|
|
432
|
+
anchor_id: str,
|
|
433
|
+
body: str,
|
|
434
|
+
parent_id: str | None = None,
|
|
435
|
+
token: Token = None,
|
|
436
|
+
) -> dict[str, Any]:
|
|
437
|
+
"""Create a comment on a plan, review, review item, or another comment."""
|
|
438
|
+
with resolver.use(token) as (c, _ctx):
|
|
439
|
+
payload = CommentCreate(
|
|
440
|
+
anchor_type=CommentAnchorType(anchor_type),
|
|
441
|
+
anchor_id=parse_uuid(anchor_id, "anchor_id"),
|
|
442
|
+
parent_id=parse_uuid(parent_id, "parent_id") if parent_id else None,
|
|
443
|
+
body=body,
|
|
444
|
+
)
|
|
445
|
+
return dump(c.create_comment(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
446
|
+
|
|
447
|
+
@mcp.tool()
|
|
448
|
+
def list_comments(experiment_id: str, tree: bool = False, token: Token = None) -> list[dict[str, Any]]:
|
|
449
|
+
"""List comments on an experiment; set tree=true for nested structure."""
|
|
450
|
+
with resolver.use(token) as (c, _ctx):
|
|
451
|
+
return dump(c.list_comments(parse_uuid(experiment_id, "experiment_id"), tree=tree))
|
|
452
|
+
|
|
453
|
+
@mcp.tool()
|
|
454
|
+
def create_log(
|
|
455
|
+
experiment_id: str,
|
|
456
|
+
summary: str,
|
|
457
|
+
content_md: str,
|
|
458
|
+
metadata: dict[str, Any] | None = None,
|
|
459
|
+
token: Token = None,
|
|
460
|
+
) -> dict[str, Any]:
|
|
461
|
+
"""Append an execution log entry to an experiment."""
|
|
462
|
+
with resolver.use(token) as (c, _ctx):
|
|
463
|
+
payload = ExperimentLogCreate(summary=summary, content_md=content_md, metadata=metadata)
|
|
464
|
+
return dump(c.create_log(parse_uuid(experiment_id, "experiment_id"), payload))
|
|
465
|
+
|
|
466
|
+
@mcp.tool()
|
|
467
|
+
def list_logs(experiment_id: str, token: Token = None) -> list[dict[str, Any]]:
|
|
468
|
+
"""List execution logs for an experiment."""
|
|
469
|
+
with resolver.use(token) as (c, _ctx):
|
|
470
|
+
return dump(c.list_logs(parse_uuid(experiment_id, "experiment_id")))
|
|
471
|
+
|
|
472
|
+
@mcp.tool()
|
|
473
|
+
def get_global_status(project_id: str | None = None, token: Token = None) -> dict[str, Any]:
|
|
474
|
+
"""Return the global status board, optionally filtered by project UUID (Admin only)."""
|
|
475
|
+
with resolver.use(token) as (c, ctx):
|
|
476
|
+
ctx.require_admin()
|
|
477
|
+
pid = parse_uuid(project_id, "project_id") if project_id else None
|
|
478
|
+
return dump(c.get_global_status(pid))
|
|
479
|
+
|
|
480
|
+
@mcp.tool()
|
|
481
|
+
def list_projects(include_archived: bool = False, token: Token = None) -> list[dict[str, Any]]:
|
|
482
|
+
"""List all projects (Admin only)."""
|
|
483
|
+
with resolver.use(token) as (c, ctx):
|
|
484
|
+
ctx.require_admin()
|
|
485
|
+
return dump(c.list_projects(include_archived=include_archived))
|
|
486
|
+
|
|
487
|
+
@mcp.tool()
|
|
488
|
+
def get_project(project_id: str, token: Token = None) -> dict[str, Any]:
|
|
489
|
+
"""Get a single project by UUID (Admin only)."""
|
|
490
|
+
with resolver.use(token) as (c, ctx):
|
|
491
|
+
ctx.require_admin()
|
|
492
|
+
return dump(c.get_project(parse_uuid(project_id, "project_id")))
|
|
493
|
+
|
|
494
|
+
@mcp.tool()
|
|
495
|
+
def revise_project_status(
|
|
496
|
+
content_md: str,
|
|
497
|
+
project_id: str | None = None,
|
|
498
|
+
change_note: str | None = None,
|
|
499
|
+
token: Token = None,
|
|
500
|
+
) -> dict[str, Any]:
|
|
501
|
+
"""Revise the project Current Status markdown (Admin or project-bound agent)."""
|
|
502
|
+
with resolver.use(token) as (c, ctx):
|
|
503
|
+
pid = ctx.resolve_project_id(project_id)
|
|
504
|
+
payload = ProjectStatusRevise(content_md=content_md, change_note=change_note)
|
|
505
|
+
return dump(c.revise_project_status(pid, payload))
|
|
506
|
+
|
|
507
|
+
@mcp.tool()
|
|
508
|
+
def create_project(
|
|
509
|
+
name: str,
|
|
510
|
+
workspace_path: str,
|
|
511
|
+
description: str | None = None,
|
|
512
|
+
project_key: str | None = None,
|
|
513
|
+
token: Token = None,
|
|
514
|
+
) -> dict[str, Any]:
|
|
515
|
+
"""Create a new project bound to a workspace path (Admin only)."""
|
|
516
|
+
with resolver.use(token) as (c, ctx):
|
|
517
|
+
ctx.require_admin()
|
|
518
|
+
key = project_key or name.lower().replace(" ", "-")
|
|
519
|
+
return dump(c.create_project(key, name, workspace_path, description))
|
|
520
|
+
|
|
521
|
+
@mcp.resource("map://project/{project_id}/status")
|
|
522
|
+
def project_status_resource(project_id: str) -> str:
|
|
523
|
+
"""Read-only project status board snapshot (UUID form, Admin)."""
|
|
524
|
+
with resolver.use(None) as (c, ctx):
|
|
525
|
+
if not ctx.is_admin:
|
|
526
|
+
raise ValueError("Admin role required for this resource")
|
|
527
|
+
return dumps_json(c.get_project_status(parse_uuid(project_id, "project_id")))
|
|
528
|
+
|
|
529
|
+
@mcp.resource("map://experiment/{experiment_id}")
|
|
530
|
+
def experiment_context(experiment_id: str) -> str:
|
|
531
|
+
"""Read-only snapshot: experiment detail, plans, reviews, disputes, comments."""
|
|
532
|
+
with resolver.use(None) as (c, _ctx):
|
|
533
|
+
eid = parse_uuid(experiment_id, "experiment_id")
|
|
534
|
+
detail = c.get_experiment(eid)
|
|
535
|
+
plans = c.list_plans(eid)
|
|
536
|
+
reviews = c.list_reviews(eid)
|
|
537
|
+
comments = c.list_comments(eid, tree=True)
|
|
538
|
+
logs = c.list_logs(eid)
|
|
539
|
+
|
|
540
|
+
open_items: list[dict[str, Any]] = []
|
|
541
|
+
for review in reviews:
|
|
542
|
+
for item in review.items:
|
|
543
|
+
if item.kind.value == "unreasonable" and item.status and item.status.value == "open":
|
|
544
|
+
open_items.append(dump(item))
|
|
545
|
+
|
|
546
|
+
context = {
|
|
547
|
+
"experiment": dump(detail),
|
|
548
|
+
"current_plan": dump(detail.current_plan),
|
|
549
|
+
"plan_versions": dump(plans),
|
|
550
|
+
"reviews": dump(reviews),
|
|
551
|
+
"open_unreasonable_items": open_items,
|
|
552
|
+
"comments_tree": dump(comments),
|
|
553
|
+
"logs": dump(logs),
|
|
554
|
+
}
|
|
555
|
+
return dumps_json(context)
|
|
556
|
+
|
|
557
|
+
@mcp.resource("map://project/{project_key}/current-status")
|
|
558
|
+
def project_current_status_resource(project_key: str) -> str:
|
|
559
|
+
"""Read-only project Current Status."""
|
|
560
|
+
with resolver.use(None) as (c, _ctx):
|
|
561
|
+
project = c.get_project_by_key(project_key)
|
|
562
|
+
return dumps_json(c.get_project_status(project.id))
|
|
563
|
+
|
|
564
|
+
_original_streamable_http_app = mcp.streamable_http_app
|
|
565
|
+
|
|
566
|
+
def streamable_http_app_with_bearer():
|
|
567
|
+
app = _original_streamable_http_app()
|
|
568
|
+
app.add_middleware(BearerTokenMiddleware)
|
|
569
|
+
return app
|
|
570
|
+
|
|
571
|
+
mcp.streamable_http_app = streamable_http_app_with_bearer # type: ignore[method-assign]
|
|
572
|
+
|
|
573
|
+
return mcp
|
map_mcp/session.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from map_client.client import MAPClient
|
|
8
|
+
|
|
9
|
+
from map_mcp.auth import get_request_bearer
|
|
10
|
+
from map_mcp.context import AgentContext
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
_TOKEN_PARAM_HELP = (
|
|
16
|
+
"Deprecated. Prefer Authorization: Bearer <MAP agent token> on the MCP HTTP connection, "
|
|
17
|
+
"or MAP_TOKEN in env for stdio. Overrides transport auth when set."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def token_param() -> str:
|
|
22
|
+
"""Shared description for the optional token tool parameter."""
|
|
23
|
+
return _TOKEN_PARAM_HELP
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClientResolver:
|
|
27
|
+
"""Resolve MAPClient + AgentContext from transport Bearer, stdio default, or per-call token."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_url: str,
|
|
32
|
+
default_client: MAPClient | None = None,
|
|
33
|
+
*,
|
|
34
|
+
transport: httpx.BaseTransport | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.api_url = api_url.rstrip("/")
|
|
37
|
+
self._default_client = default_client
|
|
38
|
+
self._default_ctx = AgentContext.from_client(default_client) if default_client else None
|
|
39
|
+
if transport is not None:
|
|
40
|
+
self._transport = transport
|
|
41
|
+
elif default_client is not None:
|
|
42
|
+
self._transport = default_client._transport
|
|
43
|
+
else:
|
|
44
|
+
self._transport = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def default_ctx(self) -> AgentContext | None:
|
|
48
|
+
return self._default_ctx
|
|
49
|
+
|
|
50
|
+
def _resolve_token(self, token: str | None) -> str | None:
|
|
51
|
+
if token:
|
|
52
|
+
return token
|
|
53
|
+
bearer = get_request_bearer()
|
|
54
|
+
if bearer:
|
|
55
|
+
return bearer
|
|
56
|
+
if self._default_client is not None:
|
|
57
|
+
return self._default_client.token
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
@contextmanager
|
|
61
|
+
def use(self, token: str | None) -> Iterator[tuple[MAPClient, AgentContext]]:
|
|
62
|
+
effective = self._resolve_token(token)
|
|
63
|
+
if not effective:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Authentication required: set Authorization: Bearer <MAP agent token> "
|
|
66
|
+
"(HTTP) or MAP_TOKEN (stdio)"
|
|
67
|
+
)
|
|
68
|
+
if (
|
|
69
|
+
self._default_client is not None
|
|
70
|
+
and effective == self._default_client.token
|
|
71
|
+
and self._default_ctx is not None
|
|
72
|
+
):
|
|
73
|
+
yield self._default_client, self._default_ctx
|
|
74
|
+
return
|
|
75
|
+
client = MAPClient(self.api_url, effective, transport=self._transport)
|
|
76
|
+
try:
|
|
77
|
+
yield client, AgentContext.from_client(client)
|
|
78
|
+
finally:
|
|
79
|
+
client.close()
|
map_sdk/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""map_sdk — shared, server-agnostic utilities for CLI + SDK.
|
|
2
|
+
|
|
3
|
+
arch experiment (0519e2a3) PR1: skeleton. PR2 added ``map_sdk.evidence``
|
|
4
|
+
with ``metadata_has_completion_evidence`` + ``EVIDENCE_METADATA_KEYS``
|
|
5
|
+
moved out of ``server.services.evidence_service``.
|
|
6
|
+
|
|
7
|
+
Hard boundary: this package MUST NOT import from ``server.*``. CI
|
|
8
|
+
asserts that via grep + unit test.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from map_sdk.evidence import (
|
|
12
|
+
EVIDENCE_METADATA_KEYS,
|
|
13
|
+
metadata_has_completion_evidence,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def hello() -> str:
|
|
20
|
+
"""Smoke function so the import surface has at least one symbol."""
|
|
21
|
+
return "map_sdk ok"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"hello",
|
|
27
|
+
"EVIDENCE_METADATA_KEYS",
|
|
28
|
+
"metadata_has_completion_evidence",
|
|
29
|
+
]
|