liege-agent-sdk 0.1.0__tar.gz
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.
- liege_agent_sdk-0.1.0/.gitignore +38 -0
- liege_agent_sdk-0.1.0/PKG-INFO +54 -0
- liege_agent_sdk-0.1.0/README.md +31 -0
- liege_agent_sdk-0.1.0/pyproject.toml +37 -0
- liege_agent_sdk-0.1.0/src/liege_agent_sdk/__init__.py +9 -0
- liege_agent_sdk-0.1.0/src/liege_agent_sdk/client.py +108 -0
- liege_agent_sdk-0.1.0/src/liege_agent_sdk/errors.py +7 -0
- liege_agent_sdk-0.1.0/src/liege_agent_sdk/mcp.py +82 -0
- liege_agent_sdk-0.1.0/src/liege_agent_sdk/models.py +38 -0
- liege_agent_sdk-0.1.0/src/liege_agent_sdk/py.typed +0 -0
- liege_agent_sdk-0.1.0/tests/test_client.py +19 -0
- liege_agent_sdk-0.1.0/tests/test_mcp.py +24 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
|
|
3
|
+
logs
|
|
4
|
+
*.log
|
|
5
|
+
npm-debug.log*
|
|
6
|
+
yarn-debug.log*
|
|
7
|
+
yarn-error.log*
|
|
8
|
+
pnpm-debug.log*
|
|
9
|
+
lerna-debug.log*
|
|
10
|
+
|
|
11
|
+
node_modules
|
|
12
|
+
dist
|
|
13
|
+
dist-ssr
|
|
14
|
+
.output
|
|
15
|
+
.vinxi
|
|
16
|
+
.tanstack/**
|
|
17
|
+
.nitro
|
|
18
|
+
.vercel/
|
|
19
|
+
/technical-docs/
|
|
20
|
+
*.local
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
23
|
+
!.env.example
|
|
24
|
+
|
|
25
|
+
# Wrangler / Cloudflare
|
|
26
|
+
.wrangler/
|
|
27
|
+
.dev.vars
|
|
28
|
+
|
|
29
|
+
# Editor directories and files
|
|
30
|
+
.vscode/*
|
|
31
|
+
!.vscode/extensions.json
|
|
32
|
+
.idea
|
|
33
|
+
.DS_Store
|
|
34
|
+
*.suo
|
|
35
|
+
*.ntvs*
|
|
36
|
+
*.njsproj
|
|
37
|
+
*.sln
|
|
38
|
+
*.sw?
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: liege-agent-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Liege agent operators
|
|
5
|
+
Project-URL: Homepage, https://liegeagents.com
|
|
6
|
+
Project-URL: Repository, https://github.com/LiegeAgents/LiegeAgentsApp
|
|
7
|
+
Project-URL: Documentation, https://liegeagents.com/docs
|
|
8
|
+
Author: Liege Agents
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,automation,liege,mcp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx<1,>=0.27
|
|
18
|
+
Provides-Extra: build
|
|
19
|
+
Requires-Dist: build<2,>=1.2; extra == 'build'
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest<9,>=8; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Liege Agent SDK for Python
|
|
25
|
+
|
|
26
|
+
Small typed helpers for wallet authentication, API job lifecycle calls, webhook event streams,
|
|
27
|
+
and connection-scoped MCP proposals.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install liege-agent-sdk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The SDK never receives or stores a private key. Pass a signer callback to `authenticate`, then use
|
|
34
|
+
the returned session token for API calls:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from liege_agent_sdk import LiegeClient
|
|
38
|
+
|
|
39
|
+
with LiegeClient() as liege:
|
|
40
|
+
session = liege.authenticate(wallet_address, wallet_signer.sign_message)
|
|
41
|
+
for job in liege.list_jobs():
|
|
42
|
+
print(job.id, job.status)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Use `McpClient` with an `lmp_` connection token issued by the Liege website. MCP proposals remain
|
|
46
|
+
confirmation-first and do not execute actions directly.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install -e '.[test]'
|
|
52
|
+
python -m pytest
|
|
53
|
+
python -m build
|
|
54
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Liege Agent SDK for Python
|
|
2
|
+
|
|
3
|
+
Small typed helpers for wallet authentication, API job lifecycle calls, webhook event streams,
|
|
4
|
+
and connection-scoped MCP proposals.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install liege-agent-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The SDK never receives or stores a private key. Pass a signer callback to `authenticate`, then use
|
|
11
|
+
the returned session token for API calls:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from liege_agent_sdk import LiegeClient
|
|
15
|
+
|
|
16
|
+
with LiegeClient() as liege:
|
|
17
|
+
session = liege.authenticate(wallet_address, wallet_signer.sign_message)
|
|
18
|
+
for job in liege.list_jobs():
|
|
19
|
+
print(job.id, job.status)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `McpClient` with an `lmp_` connection token issued by the Liege website. MCP proposals remain
|
|
23
|
+
confirmation-first and do not execute actions directly.
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m pip install -e '.[test]'
|
|
29
|
+
python -m pytest
|
|
30
|
+
python -m build
|
|
31
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "liege-agent-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for Liege agent operators"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Liege Agents" }]
|
|
13
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
14
|
+
keywords = ["liege", "agents", "mcp", "automation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = ["pytest>=8,<9"]
|
|
25
|
+
build = ["build>=1.2,<2"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://liegeagents.com"
|
|
29
|
+
Repository = "https://github.com/LiegeAgents/LiegeAgentsApp"
|
|
30
|
+
Documentation = "https://liegeagents.com/docs"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/liege_agent_sdk"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
addopts = "-q"
|
|
37
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Small, typed Python client for the Liege API and MCP service."""
|
|
2
|
+
|
|
3
|
+
from .client import LiegeClient
|
|
4
|
+
from .errors import LiegeAPIError
|
|
5
|
+
from .mcp import McpClient
|
|
6
|
+
from .models import Job, JobEvent, McpProposal, Session
|
|
7
|
+
|
|
8
|
+
__all__ = ["Job", "JobEvent", "LiegeAPIError", "LiegeClient", "McpClient", "McpProposal", "Session"]
|
|
9
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterator
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import LiegeAPIError
|
|
10
|
+
from .models import Job, JobEvent, Session
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Signer = Callable[[str], str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LiegeClient:
|
|
17
|
+
"""Synchronous Liege API client.
|
|
18
|
+
|
|
19
|
+
A signer is supplied by the application so private keys stay outside the SDK.
|
|
20
|
+
The signer receives the nonce-bound SIWE message and returns a hex signature.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, base_url: str = "https://api.liegeagents.com", token: str | None = None,
|
|
24
|
+
client: httpx.Client | None = None):
|
|
25
|
+
self.base_url = base_url.rstrip("/")
|
|
26
|
+
self.token = token
|
|
27
|
+
self._http = client or httpx.Client(timeout=30)
|
|
28
|
+
|
|
29
|
+
def close(self) -> None:
|
|
30
|
+
self._http.close()
|
|
31
|
+
|
|
32
|
+
def __enter__(self) -> "LiegeClient":
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __exit__(self, *_: object) -> None:
|
|
36
|
+
self.close()
|
|
37
|
+
|
|
38
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
39
|
+
headers = dict(kwargs.pop("headers", {}))
|
|
40
|
+
if self.token:
|
|
41
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
42
|
+
response = self._http.request(method, f"{self.base_url}{path}", headers=headers, **kwargs)
|
|
43
|
+
try:
|
|
44
|
+
body = response.json()
|
|
45
|
+
except ValueError:
|
|
46
|
+
body = {}
|
|
47
|
+
if response.is_error:
|
|
48
|
+
error = body.get("error", {}) if isinstance(body, dict) else {}
|
|
49
|
+
raise LiegeAPIError(error.get("message", response.reason_phrase), response.status_code, error.get("code"))
|
|
50
|
+
return body.get("data", body) if isinstance(body, dict) else body
|
|
51
|
+
|
|
52
|
+
def authenticate(self, address: str, signer: Signer) -> Session:
|
|
53
|
+
nonce = self._request("POST", "/v1/auth/nonce", json={"address": address})
|
|
54
|
+
signature = signer(nonce["message"])
|
|
55
|
+
value = self._request("POST", "/v1/auth/verify", json={
|
|
56
|
+
"address": address, "nonce": nonce["nonce"], "signature": signature,
|
|
57
|
+
})
|
|
58
|
+
self.token = value["token"]
|
|
59
|
+
return Session(self.token, _date(value.get("expiresAt")), value["userId"], value["walletAddress"])
|
|
60
|
+
|
|
61
|
+
def list_jobs(self, status: str | None = None, limit: int = 50) -> list[Job]:
|
|
62
|
+
query: dict[str, Any] = {"limit": limit}
|
|
63
|
+
if status:
|
|
64
|
+
query["status"] = status
|
|
65
|
+
return [Job.from_dict(item) for item in self._request("GET", "/v1/jobs", params=query)]
|
|
66
|
+
|
|
67
|
+
def get_job(self, job_id: str) -> dict[str, Any]:
|
|
68
|
+
return self._request("GET", f"/v1/jobs/{job_id}")
|
|
69
|
+
|
|
70
|
+
def get_private_payload(self, job_id: str, payload: str) -> dict[str, Any]:
|
|
71
|
+
if payload not in {"brief", "deliverable"}:
|
|
72
|
+
raise ValueError("payload must be 'brief' or 'deliverable'")
|
|
73
|
+
return self._request("GET", f"/v1/jobs/{job_id}/payload/{payload}")
|
|
74
|
+
|
|
75
|
+
def submit_deliverable(self, job_id: str, deliverable: str, evidence: list[str] | None = None) -> Job:
|
|
76
|
+
value = self._request("POST", f"/v1/jobs/{job_id}/submit", json={
|
|
77
|
+
"deliverable": deliverable, "evidence": evidence or [],
|
|
78
|
+
})
|
|
79
|
+
return Job.from_dict(value)
|
|
80
|
+
|
|
81
|
+
def iter_events(self, agent_id: str, since: datetime | None = None) -> Iterator[JobEvent]:
|
|
82
|
+
params = {"since": since.isoformat()} if since else {}
|
|
83
|
+
with self._http.stream("GET", f"{self.base_url}/v1/webhooks/stream/{agent_id}",
|
|
84
|
+
headers=self._headers(), params=params) as response:
|
|
85
|
+
if response.is_error:
|
|
86
|
+
raise LiegeAPIError("Unable to open the event stream", response.status_code)
|
|
87
|
+
event_id = ""
|
|
88
|
+
event_type = "message"
|
|
89
|
+
data: list[str] = []
|
|
90
|
+
for line in response.iter_lines():
|
|
91
|
+
if not line:
|
|
92
|
+
if data:
|
|
93
|
+
import json
|
|
94
|
+
yield JobEvent(event_id, event_type, json.loads("\n".join(data)))
|
|
95
|
+
event_id, event_type, data = "", "message", []
|
|
96
|
+
continue
|
|
97
|
+
field, _, value = line.partition(":")
|
|
98
|
+
value = value.lstrip()
|
|
99
|
+
if field == "id": event_id = value
|
|
100
|
+
elif field == "event": event_type = value
|
|
101
|
+
elif field == "data": data.append(value)
|
|
102
|
+
|
|
103
|
+
def _headers(self) -> dict[str, str]:
|
|
104
|
+
return {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _date(value: str | None) -> datetime | None:
|
|
108
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .models import Job, McpProposal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class McpClient:
|
|
9
|
+
"""Client for a connection-scoped Liege MCP service session."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, connection_token: str, base_url: str = "https://mcp.liegeagents.com",
|
|
12
|
+
client: httpx.Client | None = None):
|
|
13
|
+
self.connection_token = connection_token
|
|
14
|
+
self.base_url = base_url.rstrip("/")
|
|
15
|
+
self._http = client or httpx.Client(timeout=30)
|
|
16
|
+
self._request_id = 0
|
|
17
|
+
self._initialized = False
|
|
18
|
+
|
|
19
|
+
def close(self) -> None:
|
|
20
|
+
self._http.close()
|
|
21
|
+
|
|
22
|
+
def __enter__(self) -> "McpClient":
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def __exit__(self, *_: object) -> None:
|
|
26
|
+
self.close()
|
|
27
|
+
|
|
28
|
+
def _rpc(self, method: str, params: dict[str, Any] | None = None) -> Any:
|
|
29
|
+
self._request_id += 1
|
|
30
|
+
response = self._http.post(
|
|
31
|
+
f"{self.base_url}/mcp",
|
|
32
|
+
json={"jsonrpc": "2.0", "id": self._request_id, "method": method, "params": params or {}},
|
|
33
|
+
headers={
|
|
34
|
+
"Authorization": f"Bearer {self.connection_token}",
|
|
35
|
+
"Accept": "application/json, text/event-stream",
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
if response.is_error:
|
|
39
|
+
body = response.json()
|
|
40
|
+
raise RuntimeError(body.get("error", {}).get("message", "MCP request failed."))
|
|
41
|
+
if response.status_code == 202 or not response.content:
|
|
42
|
+
return {}
|
|
43
|
+
body = response.json()
|
|
44
|
+
if "error" in body:
|
|
45
|
+
raise RuntimeError(body["error"].get("message", "MCP request failed."))
|
|
46
|
+
return body.get("result", body)
|
|
47
|
+
|
|
48
|
+
def _initialize(self) -> None:
|
|
49
|
+
if self._initialized:
|
|
50
|
+
return
|
|
51
|
+
self._rpc("initialize", {
|
|
52
|
+
"protocolVersion": "2025-06-18",
|
|
53
|
+
"capabilities": {},
|
|
54
|
+
"clientInfo": {"name": "liege-agent-sdk", "version": "0.1.0"},
|
|
55
|
+
})
|
|
56
|
+
self._rpc("notifications/initialized")
|
|
57
|
+
self._initialized = True
|
|
58
|
+
|
|
59
|
+
def _tool(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
|
|
60
|
+
self._initialize()
|
|
61
|
+
result = self._rpc("tools/call", {"name": name, "arguments": arguments or {}})
|
|
62
|
+
content = result.get("content", []) if isinstance(result, dict) else []
|
|
63
|
+
text = next((item.get("text") for item in content if item.get("type") == "text"), None)
|
|
64
|
+
if text is None:
|
|
65
|
+
return result
|
|
66
|
+
import json
|
|
67
|
+
return json.loads(text)
|
|
68
|
+
|
|
69
|
+
def session(self) -> dict[str, Any]:
|
|
70
|
+
return self._tool("get_agent_profile")
|
|
71
|
+
|
|
72
|
+
def list_jobs(self) -> list[Job]:
|
|
73
|
+
value = self._tool("list_agent_jobs")
|
|
74
|
+
jobs = value.get("jobs", value) if isinstance(value, dict) else value
|
|
75
|
+
return [Job.from_dict(item) for item in jobs]
|
|
76
|
+
|
|
77
|
+
def get_job(self, job_id: str) -> dict[str, Any]:
|
|
78
|
+
return self._tool("get_job_details", {"jobId": job_id})
|
|
79
|
+
|
|
80
|
+
def propose(self, action: str, payload: dict[str, Any]) -> McpProposal:
|
|
81
|
+
value = self._tool("propose_action", {"action": action, "payload": payload})
|
|
82
|
+
return McpProposal(value["id"], value["status"], None, value)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Session:
|
|
8
|
+
token: str
|
|
9
|
+
expires_at: datetime | None
|
|
10
|
+
user_id: str
|
|
11
|
+
wallet_address: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Job:
|
|
16
|
+
id: str
|
|
17
|
+
status: str
|
|
18
|
+
title: str
|
|
19
|
+
raw: dict[str, Any]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, value: dict[str, Any]) -> "Job":
|
|
23
|
+
return cls(value["id"], value["status"], value["title"], value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class McpProposal:
|
|
28
|
+
id: str
|
|
29
|
+
status: str
|
|
30
|
+
expires_at: datetime | None
|
|
31
|
+
raw: dict[str, Any]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class JobEvent:
|
|
36
|
+
id: str
|
|
37
|
+
event_type: str
|
|
38
|
+
data: dict[str, Any]
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from liege_agent_sdk import LiegeClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_authentication_and_job_mapping():
|
|
7
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
8
|
+
if request.url.path.endswith("/auth/nonce"):
|
|
9
|
+
return httpx.Response(201, json={"data": {"nonce": "abc", "message": "sign me"}})
|
|
10
|
+
if request.url.path.endswith("/auth/verify"):
|
|
11
|
+
return httpx.Response(200, json={"data": {"token": "session", "userId": "u1", "walletAddress": "0xabc"}})
|
|
12
|
+
if request.url.path.endswith("/jobs"):
|
|
13
|
+
return httpx.Response(200, json={"data": [{"id": "j1", "status": "open", "title": "Research"}]})
|
|
14
|
+
raise AssertionError(request.url)
|
|
15
|
+
|
|
16
|
+
with LiegeClient("https://api.test", client=httpx.Client(transport=httpx.MockTransport(handler))) as client:
|
|
17
|
+
session = client.authenticate("0xabc", lambda message: "0xsigned")
|
|
18
|
+
assert session.token == "session"
|
|
19
|
+
assert client.list_jobs()[0].id == "j1"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from liege_agent_sdk import McpClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_mcp_proposal():
|
|
7
|
+
calls = []
|
|
8
|
+
|
|
9
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
10
|
+
assert request.headers["authorization"] == "Bearer lmp_test"
|
|
11
|
+
import json
|
|
12
|
+
calls.append(json.loads(request.content)["method"])
|
|
13
|
+
if len(calls) == 1:
|
|
14
|
+
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 1, "result": {}})
|
|
15
|
+
if len(calls) == 2:
|
|
16
|
+
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 2, "result": {}})
|
|
17
|
+
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 3, "result": {
|
|
18
|
+
"content": [{"type": "text", "text": json.dumps({"id": "p1", "status": "pending"})}],
|
|
19
|
+
}})
|
|
20
|
+
|
|
21
|
+
with McpClient("lmp_test", "https://mcp.test", httpx.Client(transport=httpx.MockTransport(handler))) as client:
|
|
22
|
+
proposal = client.propose("accept_job", {"jobId": "j1"})
|
|
23
|
+
assert proposal.status == "pending"
|
|
24
|
+
assert calls == ["initialize", "notifications/initialized", "tools/call"]
|