From 60283d55fc4a9cacd7c76c44329884b925dba1d3 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 04:19:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=8F=E9=AA=8C=E5=A4=8D=E7=94=A8=20?= =?UTF-8?q?MCP=20=E6=9A=B4=E9=9C=B2=E2=80=94=E2=80=94=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=E5=88=97/=E5=BB=BA/=E5=BA=9F=E5=BC=83?= =?UTF-8?q?=EF=BC=88=E6=B3=A8=E5=85=A5=E4=B8=8E=E6=8F=90=E5=8F=96=E5=B7=B2?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=EF=BC=8C=E4=B8=8D=E9=87=8D=E5=A4=8D=E9=80=A0?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloud/mcp_server.py | 37 ++++++++++++++++++++++++++++++++++ cloud/tests/test_mcp_server.py | 29 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/cloud/mcp_server.py b/cloud/mcp_server.py index b91964a..ef5d4f0 100644 --- a/cloud/mcp_server.py +++ b/cloud/mcp_server.py @@ -106,6 +106,17 @@ TOOLS = [ {"project_id": {"type": "string"}}, ["project_id"]), _tool("get_models", "取模型一览(价目与默认模型,服务端口径)。", {}, []), + _tool("list_project_memories", "列出某项目的项目级记忆(此前任务沉淀的经验/约定,新任务会自动注入;top_k 可选,默认 20)。", + {"project_id": {"type": "string"}, + "top_k": {"type": "integer"}}, ["project_id"]), + _tool("create_project_memory", "给某项目记一条项目级记忆(kind 与 content 必填)。", + {"project_id": {"type": "string"}, + "kind": {"type": "string"}, + "content": {"type": "string"}}, ["project_id", "kind", "content"]), + _tool("supersede_project_memory", "废弃某项目的一条记忆,可同时给替代内容(不传则只废弃)。", + {"project_id": {"type": "string"}, + "memory_id": {"type": "string"}, + "replacement_content": {"type": "string"}}, ["project_id", "memory_id"]), ] @@ -239,6 +250,32 @@ class MCPServer: "GET", "/projects/" + args["project_id"] + "/members")) if name == "get_models": return self._wrap(*self.transport("GET", "/models")) + if name == "list_project_memories": + if not args.get("project_id"): + return True, {"error": "project_id required"} + path = "/projects/" + args["project_id"] + "/identity-memories" + if args.get("top_k") is not None: + path += "?" + urllib.parse.urlencode( + {"top_k": args["top_k"]}) + return self._wrap(*self.transport("GET", path)) + if name == "create_project_memory": + if not args.get("project_id") or not args.get("kind") \ + or not args.get("content"): + return True, {"error": "project_id, kind and content required"} + return self._wrap(*self.transport( + "POST", "/projects/" + args["project_id"] + + "/identity-memories", + {"kind": args["kind"], "content": args["content"]})) + if name == "supersede_project_memory": + if not args.get("project_id") or not args.get("memory_id"): + return True, {"error": "project_id and memory_id required"} + body = {} + if args.get("replacement_content") is not None: + body["replacement_content"] = args["replacement_content"] + return self._wrap(*self.transport( + "POST", "/projects/" + args["project_id"] + + "/identity-memories/" + args["memory_id"] + "/supersede", + body)) return True, {"error": "unknown tool: %s" % name} @staticmethod diff --git a/cloud/tests/test_mcp_server.py b/cloud/tests/test_mcp_server.py index afba3c7..d1611d6 100644 --- a/cloud/tests/test_mcp_server.py +++ b/cloud/tests/test_mcp_server.py @@ -65,6 +65,13 @@ class FakeTransport: return 200, {"members": []} if path == "/models": return 200, {"models": []} + if path == "/projects/p1/identity-memories": + return 200, {"memories": [{"id": "m1", "content": "x"}]} + if path == "/projects/p1/identity-memories?top_k=3": + return 200, {"memories": []} + if path == "/projects/p1/identity-memories/m1/supersede" \ + and method == "POST": + return 200, {"id": "m1", "status": "superseded"} if path == "/tasks/nope": return 404, {"error": "not found"} return 400, {"error": "bad request"} @@ -179,6 +186,28 @@ class MCPServerTestCase(unittest.TestCase): self.assertFalse(is_error, name) self.assertEqual(self.transport.calls[-1][:2], expected, name) is_error, _ = self.server.call_tool("create_followup", {"task_id": "t1"}) + def test_project_memory_tools_translate_to_rest(self): + is_error, payload = self.server.call_tool( + "list_project_memories", {"project_id": "p1", "top_k": 3}) + self.assertFalse(is_error) + self.assertEqual(self.transport.calls[-1][:2], + ("GET", "/projects/p1/identity-memories?top_k=3")) + is_error, _ = self.server.call_tool( + "create_project_memory", + {"project_id": "p1", "kind": "convention", "content": "先跑测试"}) + self.assertFalse(is_error) + self.assertEqual(self.transport.calls[-1][:2], + ("POST", "/projects/p1/identity-memories")) + is_error, _ = self.server.call_tool( + "supersede_project_memory", + {"project_id": "p1", "memory_id": "m1", + "replacement_content": "新约定"}) + self.assertFalse(is_error) + self.assertEqual(self.transport.calls[-1][:2], + ("POST", + "/projects/p1/identity-memories/m1/supersede")) + is_error, _ = self.server.call_tool( + "create_project_memory", {"project_id": "p1", "kind": "k"}) self.assertTrue(is_error) def test_missing_arg_is_error_not_rest_call(self):