lcp-mcp-server 1.0.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.
- lcp_mcp_server-1.0.0/.gitignore +6 -0
- lcp_mcp_server-1.0.0/PKG-INFO +11 -0
- lcp_mcp_server-1.0.0/README.md +91 -0
- lcp_mcp_server-1.0.0/lcp_mcp_server/__init__.py +5 -0
- lcp_mcp_server-1.0.0/lcp_mcp_server/client.py +87 -0
- lcp_mcp_server-1.0.0/lcp_mcp_server/schema_loader.py +47 -0
- lcp_mcp_server-1.0.0/lcp_mcp_server/server.py +258 -0
- lcp_mcp_server-1.0.0/pyproject.toml +23 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lcp-mcp-server
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LCP (Lead Context Protocol) MCP server — thin adapter wrapping LCP REST endpoints as agent tools.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: jsonschema>=4.20.0
|
|
9
|
+
Requires-Dist: lcp-sdk>=1.0.0
|
|
10
|
+
Requires-Dist: mcp<2.1.0,>=1.28.1
|
|
11
|
+
Requires-Dist: referencing>=0.35.0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# LCP MCP Server — Reference Implementation
|
|
2
|
+
|
|
3
|
+
A thin MCP (Model Context Protocol) adapter that exposes LCP REST endpoints
|
|
4
|
+
as agent tools. The core protocol is REST+JSON; this server wraps it so AI
|
|
5
|
+
agents can interact with any LCP-compliant endpoint via MCP. It does not
|
|
6
|
+
host an LCP platform, persist leads, receive webhooks, or run auctions.
|
|
7
|
+
|
|
8
|
+
> **Architecture:** The LCP core never depends on MCP. This server is a
|
|
9
|
+
> stateless adapter — it translates MCP tool calls into HTTP requests and
|
|
10
|
+
> HTTP responses into MCP tool results. It works with any LCP-compliant
|
|
11
|
+
> endpoint (any LCP-compliant REST API).
|
|
12
|
+
|
|
13
|
+
## Tools
|
|
14
|
+
|
|
15
|
+
| Tool | LCP Endpoint | Description |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `submit_lead` | `POST /v1/lcp/leads` | Submit a lead message |
|
|
18
|
+
| `submit_call` | `POST /v1/lcp/calls` | Submit a call lead message |
|
|
19
|
+
| `query_lead_status` | `GET /v1/lcp/leads/{lead_id}` | Query lead status and lifecycle |
|
|
20
|
+
| `get_schema` | `GET /v1/lcp/schemas/{name}` | Retrieve a JSON Schema (envelope, core, message type, offer, vertical) |
|
|
21
|
+
| `get_capabilities` | `GET /v1/lcp/capabilities` | Discover endpoint capabilities (versions, verticals, countries) |
|
|
22
|
+
| `list_offers` | `GET /v1/lcp/offers` | List active offers for the authenticated sender |
|
|
23
|
+
| `submit_bid` | `POST /v1/lcp/bids` | Submit a bid in response to a ping |
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# From the repository root while packages are unreleased.
|
|
29
|
+
python3 -m pip install -e implementations/sdk/python -e implementations/mcp-server
|
|
30
|
+
|
|
31
|
+
# After the packages are published, install the adapter and its shared SDK.
|
|
32
|
+
python3 -m pip install lcp-mcp-server lcp-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The adapter delegates HTTP, HMAC signing, retries, and raw-body security
|
|
36
|
+
primitives to `lcp-sdk`; it only provides the MCP tool binding and local schema
|
|
37
|
+
fallback.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
The server reads its target endpoint from environment variables:
|
|
42
|
+
|
|
43
|
+
| Variable | Default | Description |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `LCP_ENDPOINT` | `http://localhost:8000` | Base URL of the LCP-compliant REST API |
|
|
46
|
+
| `LCP_API_KEY` | (none) | Bearer token for authentication |
|
|
47
|
+
| `LCP_HMAC_SECRET` | (none) | HMAC shared secret (if using HMAC auth instead of API key) |
|
|
48
|
+
| `LCP_SENDER_ID` | (none) | Sender ID to use in outgoing messages and read requests |
|
|
49
|
+
|
|
50
|
+
## Authentication profile
|
|
51
|
+
|
|
52
|
+
HMAC requests use the canonical signing input:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
<timestamp>\n<idempotency-key>\n<raw-request-body>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The adapter sends `X-LCP-Sender-Id`, `X-LCP-Timestamp`,
|
|
59
|
+
`X-LCP-Idempotency-Key` for mutating requests, and `X-LCP-Signature`. Bearer
|
|
60
|
+
requests use `Authorization: Bearer <token>`.
|
|
61
|
+
|
|
62
|
+
## Run
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Stdio transport (for MCP clients like Claude Desktop, Hermes)
|
|
66
|
+
lcp-mcp-server
|
|
67
|
+
|
|
68
|
+
# Or via Python module
|
|
69
|
+
python -m lcp_mcp_server.server
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## MCP Client Configuration
|
|
73
|
+
|
|
74
|
+
Add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json`):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"lcp": {
|
|
80
|
+
"command": "lcp-mcp-server",
|
|
81
|
+
"env": {
|
|
82
|
+
"LCP_ENDPOINT": "https://api.example.com",
|
|
83
|
+
"LCP_API_KEY": "your-api-key",
|
|
84
|
+
"LCP_SENDER_ID": "your-sender-id"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## LicenseApache 2.0 — same as the LCP specification. See the [SDK support policy](../../docs/SDK-ROADMAP.md) for the MCP relationship and package compatibility.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""LCP HTTP client compatibility wrapper for the MCP adapter.
|
|
2
|
+
|
|
3
|
+
The MCP package intentionally delegates authentication, retries, and HTTP
|
|
4
|
+
transport to the standalone lcp-sdk package. This wrapper preserves the MCP
|
|
5
|
+
adapter's historical ``post``/``get`` response shape, including ``_ok`` and
|
|
6
|
+
``_status_code`` metadata.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from lcp_sdk import LCPClient as SDKLCPClient
|
|
15
|
+
from lcp_sdk import LCPHTTPError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LCPClient:
|
|
19
|
+
"""MCP-facing compatibility wrapper around :class:`lcp_sdk.LCPClient`."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
endpoint: str | None = None,
|
|
24
|
+
api_key: str | None = None,
|
|
25
|
+
hmac_secret: str | None = None,
|
|
26
|
+
sender_id: str | None = None,
|
|
27
|
+
timeout: float = 30.0,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._client = SDKLCPClient(
|
|
30
|
+
endpoint or os.environ.get("LCP_ENDPOINT", "http://localhost:8000"),
|
|
31
|
+
sender_id=sender_id or os.environ.get("LCP_SENDER_ID", ""),
|
|
32
|
+
api_key=api_key or os.environ.get("LCP_API_KEY"),
|
|
33
|
+
hmac_secret=hmac_secret or os.environ.get("LCP_HMAC_SECRET"),
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def endpoint(self) -> str:
|
|
39
|
+
return self._client.endpoint
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def sender_id(self) -> str | None:
|
|
43
|
+
return self._client.sender_id
|
|
44
|
+
|
|
45
|
+
def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
46
|
+
"""POST an LCP envelope, preserving the legacy MCP response shape."""
|
|
47
|
+
try:
|
|
48
|
+
data = self._client.request("POST", path, payload=payload)
|
|
49
|
+
return _with_metadata(data, 200, True)
|
|
50
|
+
except LCPHTTPError as exc:
|
|
51
|
+
body = exc.body if isinstance(exc.body, dict) else {"raw": exc.body}
|
|
52
|
+
return _with_metadata(body, exc.status_code, False)
|
|
53
|
+
|
|
54
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
55
|
+
"""GET from the LCP endpoint, preserving the legacy response shape."""
|
|
56
|
+
try:
|
|
57
|
+
data = self._client.request("GET", path, params=params)
|
|
58
|
+
return _with_metadata(data, 200, True)
|
|
59
|
+
except LCPHTTPError as exc:
|
|
60
|
+
body = exc.body if isinstance(exc.body, dict) else {"raw": exc.body}
|
|
61
|
+
return _with_metadata(body, exc.status_code, False)
|
|
62
|
+
|
|
63
|
+
def submit_lead(self, envelope: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
return self.post("/v1/lcp/leads", envelope)
|
|
65
|
+
|
|
66
|
+
def submit_call(self, envelope: dict[str, Any]) -> dict[str, Any]:
|
|
67
|
+
return self.post("/v1/lcp/calls", envelope)
|
|
68
|
+
|
|
69
|
+
def query_lead_status(self, lead_id: str) -> dict[str, Any]:
|
|
70
|
+
return self.get(f"/v1/lcp/leads/{lead_id}")
|
|
71
|
+
|
|
72
|
+
def get_schema(self, name: str) -> dict[str, Any]:
|
|
73
|
+
return self.get(f"/v1/lcp/schemas/{name}")
|
|
74
|
+
|
|
75
|
+
def get_capabilities(self) -> dict[str, Any]:
|
|
76
|
+
return self.get("/v1/lcp/capabilities")
|
|
77
|
+
|
|
78
|
+
def list_offers(self, vertical: str | None = None) -> dict[str, Any]:
|
|
79
|
+
params = {"vertical": vertical} if vertical else None
|
|
80
|
+
return self.get("/v1/lcp/offers", params=params)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _with_metadata(data: dict[str, Any], status_code: int, ok: bool) -> dict[str, Any]:
|
|
84
|
+
result = dict(data)
|
|
85
|
+
result["_status_code"] = status_code
|
|
86
|
+
result["_ok"] = ok
|
|
87
|
+
return result
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Schema loader — reads JSON Schemas from the LCP repo.
|
|
2
|
+
|
|
3
|
+
When the REST endpoint is unavailable (e.g. local development), the MCP server
|
|
4
|
+
can serve schemas directly from the repo's schemas/ and verticals/ directories.
|
|
5
|
+
|
|
6
|
+
LCP — Lead Context Protocol. Created by Spear Systems (a Spear company).
|
|
7
|
+
Open standard — Apache 2.0, free to implement.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
# implementations/mcp-server/ -> ../../schemas, ../../verticals
|
|
17
|
+
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
18
|
+
SCHEMAS_DIR = _REPO_ROOT / "schemas"
|
|
19
|
+
VERTICALS_DIR = _REPO_ROOT / "verticals"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_schemas() -> list[str]:
|
|
23
|
+
"""List all available schema names."""
|
|
24
|
+
names: list[str] = []
|
|
25
|
+
if SCHEMAS_DIR.exists():
|
|
26
|
+
names.extend(f.stem for f in SCHEMAS_DIR.glob("*.json"))
|
|
27
|
+
if VERTICALS_DIR.exists():
|
|
28
|
+
names.extend(f"verticals/{f.stem}" for f in VERTICALS_DIR.glob("*.json"))
|
|
29
|
+
return sorted(names)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_schema(name: str) -> dict[str, Any] | None:
|
|
33
|
+
"""Load a schema by name. Returns None if not found.
|
|
34
|
+
|
|
35
|
+
Core/message schemas: name -> schemas/{name}.json
|
|
36
|
+
Vertical schemas: verticals/{name} -> verticals/{name}.json
|
|
37
|
+
"""
|
|
38
|
+
if name.startswith("verticals/"):
|
|
39
|
+
path = VERTICALS_DIR / f"{name.removeprefix('verticals/')}.json"
|
|
40
|
+
else:
|
|
41
|
+
path = SCHEMAS_DIR / f"{name}.json"
|
|
42
|
+
|
|
43
|
+
if not path.exists():
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
with open(path) as f:
|
|
47
|
+
return json.load(f)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""LCP MCP Server — thin adapter wrapping LCP REST endpoints as MCP tools.
|
|
2
|
+
|
|
3
|
+
Tools map 1:1 to LCP REST endpoints per SPEC.md §10:
|
|
4
|
+
submit_lead -> POST /v1/lcp/leads
|
|
5
|
+
submit_call -> POST /v1/lcp/calls
|
|
6
|
+
query_lead_status -> GET /v1/lcp/leads/{lead_id}
|
|
7
|
+
get_schema -> GET /v1/lcp/schemas/{name} (falls back to local repo)
|
|
8
|
+
get_capabilities -> GET /v1/lcp/capabilities
|
|
9
|
+
list_offers -> GET /v1/lcp/offers
|
|
10
|
+
|
|
11
|
+
The server is stateless — it creates a fresh HTTP client per tool call.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import mcp.types as types
|
|
20
|
+
from mcp.server import Server
|
|
21
|
+
from mcp.server.stdio import stdio_server
|
|
22
|
+
from lcp_sdk import build_envelope
|
|
23
|
+
|
|
24
|
+
from .client import LCPClient
|
|
25
|
+
from .schema_loader import list_schemas, load_schema
|
|
26
|
+
|
|
27
|
+
app: Server
|
|
28
|
+
|
|
29
|
+
def _make_envelope(msg_type: str, sender_id: str, receiver_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
30
|
+
"""Wrap a payload using the shared Python SDK envelope builder."""
|
|
31
|
+
return build_envelope(msg_type, sender_id, receiver_id, payload)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _tool_result(data: dict[str, Any]) -> list[types.TextContent]:
|
|
35
|
+
"""Format a dict as an MCP text content response."""
|
|
36
|
+
return [types.TextContent(type="text", text=json.dumps(data, indent=2))]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _error_result(message: str, code: str = "LCP-500") -> list[types.TextContent]:
|
|
40
|
+
"""Format an error as an MCP text content response."""
|
|
41
|
+
return _tool_result({"errors": [{"code": code, "message": message}]})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ─── Tool definitions ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
async def _list_tools() -> list[types.Tool]:
|
|
47
|
+
return [
|
|
48
|
+
types.Tool(
|
|
49
|
+
name="submit_lead",
|
|
50
|
+
description="Submit a lead to an LCP-compliant endpoint. The payload should contain lead fields (consumer, location, compliance, attributes, channel, etc.). The server wraps it in an LCP envelope.",
|
|
51
|
+
inputSchema={
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"receiver_id": {"type": "string", "description": "LCP receiver ID (e.g. 'spx')."},
|
|
55
|
+
"payload": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "Lead payload — consumer, location, compliance, provenance, attributes, channel, etc. See LCP lead schema.",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
"required": ["receiver_id", "payload"],
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
types.Tool(
|
|
64
|
+
name="submit_call",
|
|
65
|
+
description="Submit a call lead to an LCP-compliant endpoint. The payload should contain call fields (call block, consumer, location, compliance, attributes).",
|
|
66
|
+
inputSchema={
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"receiver_id": {"type": "string", "description": "LCP receiver ID."},
|
|
70
|
+
"payload": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"description": "Call payload — call block (call_id, status, durations, recording, etc.), consumer, location, compliance, attributes. See LCP call schema.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"required": ["receiver_id", "payload"],
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
types.Tool(
|
|
79
|
+
name="query_lead_status",
|
|
80
|
+
description="Query the status and lifecycle of a previously submitted lead.",
|
|
81
|
+
inputSchema={
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"lead_id": {"type": "string", "description": "The lead ID to query."},
|
|
85
|
+
},
|
|
86
|
+
"required": ["lead_id"],
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
types.Tool(
|
|
90
|
+
name="get_schema",
|
|
91
|
+
description="Retrieve a JSON Schema by name. Core schemas: envelope, core, lead, call, ping, post, ack, event, bid, offer. Vertical schemas: 'verticals/mortgage', etc. Falls back to local repo files if the REST endpoint is unavailable.",
|
|
92
|
+
inputSchema={
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"name": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Schema name (e.g. 'envelope', 'lead', 'verticals/mortgage').",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
"required": ["name"],
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
types.Tool(
|
|
104
|
+
name="get_capabilities",
|
|
105
|
+
description="Discover the capabilities of the LCP endpoint — supported versions, message types, verticals, countries, auth methods, events, conformance level.",
|
|
106
|
+
inputSchema={
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {},
|
|
109
|
+
},
|
|
110
|
+
),
|
|
111
|
+
types.Tool(
|
|
112
|
+
name="list_offers",
|
|
113
|
+
description="List active offers available to the authenticated sender. Optionally filter by vertical.",
|
|
114
|
+
inputSchema={
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"vertical": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "Filter offers by vertical (e.g. 'mortgage').",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
),
|
|
124
|
+
types.Tool(
|
|
125
|
+
name="submit_bid",
|
|
126
|
+
description="Submit a bid in response to a ping. The platform collects all bids and selects the winner. The winner receives a post with full PII.",
|
|
127
|
+
inputSchema={
|
|
128
|
+
"type": "object",
|
|
129
|
+
"properties": {
|
|
130
|
+
"ping_id": {"type": "string", "description": "ID of the ping being responded to."},
|
|
131
|
+
"decision": {"type": "string", "enum": ["accept", "reject", "pass"], "description": "accept = bid for the lead; reject = do not want; pass = decline to bid."},
|
|
132
|
+
"bid_price_cents": {"type": "integer", "minimum": 0, "description": "Bid price in cents. Required when decision = accept."},
|
|
133
|
+
"currency": {"type": "string", "description": "ISO 4217 currency code (e.g. AUD, USD)."},
|
|
134
|
+
"estimated_contact_seconds": {"type": "integer", "minimum": 0, "description": "Estimated time-to-contact in seconds."},
|
|
135
|
+
"buyer_reference": {"type": "string", "description": "Buyer's internal tracking reference."},
|
|
136
|
+
"reject_reason": {"type": "string", "description": "Why rejected (optional)."},
|
|
137
|
+
},
|
|
138
|
+
"required": ["ping_id", "decision"],
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ─── Tool call handler ───────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
async def _call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
|
|
147
|
+
client = LCPClient()
|
|
148
|
+
|
|
149
|
+
if name == "submit_lead":
|
|
150
|
+
receiver_id = arguments.get("receiver_id", "")
|
|
151
|
+
payload = arguments.get("payload", {})
|
|
152
|
+
envelope = _make_envelope("lead", client.sender_id, receiver_id, payload)
|
|
153
|
+
result = client.submit_lead(envelope)
|
|
154
|
+
return _tool_result(result)
|
|
155
|
+
|
|
156
|
+
elif name == "submit_call":
|
|
157
|
+
receiver_id = arguments.get("receiver_id", "")
|
|
158
|
+
payload = arguments.get("payload", {})
|
|
159
|
+
envelope = _make_envelope("call", client.sender_id, receiver_id, payload)
|
|
160
|
+
result = client.submit_call(envelope)
|
|
161
|
+
return _tool_result(result)
|
|
162
|
+
|
|
163
|
+
elif name == "query_lead_status":
|
|
164
|
+
lead_id = arguments.get("lead_id", "")
|
|
165
|
+
result = client.query_lead_status(lead_id)
|
|
166
|
+
return _tool_result(result)
|
|
167
|
+
|
|
168
|
+
elif name == "get_schema":
|
|
169
|
+
schema_name = arguments.get("name", "")
|
|
170
|
+
# Try the REST endpoint first
|
|
171
|
+
try:
|
|
172
|
+
result = client.get_schema(schema_name)
|
|
173
|
+
if result.get("_ok"):
|
|
174
|
+
return _tool_result(result)
|
|
175
|
+
except Exception:
|
|
176
|
+
pass # Fall back to local repo
|
|
177
|
+
# Fall back to local repo files
|
|
178
|
+
schema = load_schema(schema_name)
|
|
179
|
+
if schema:
|
|
180
|
+
return _tool_result(schema)
|
|
181
|
+
available = list_schemas()
|
|
182
|
+
return _error_result(
|
|
183
|
+
f"Schema '{schema_name}' not found. Available: {', '.join(available)}",
|
|
184
|
+
code="LCP-007",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
elif name == "get_capabilities":
|
|
188
|
+
result = client.get_capabilities()
|
|
189
|
+
return _tool_result(result)
|
|
190
|
+
|
|
191
|
+
elif name == "list_offers":
|
|
192
|
+
vertical = arguments.get("vertical")
|
|
193
|
+
result = client.list_offers(vertical)
|
|
194
|
+
return _tool_result(result)
|
|
195
|
+
|
|
196
|
+
elif name == "submit_bid":
|
|
197
|
+
ping_id = arguments.get("ping_id", "")
|
|
198
|
+
decision = arguments.get("decision", "pass")
|
|
199
|
+
bid_price = arguments.get("bid_price_cents", 0)
|
|
200
|
+
currency = arguments.get("currency", "USD")
|
|
201
|
+
payload = {
|
|
202
|
+
"ping_id": ping_id,
|
|
203
|
+
"decision": decision,
|
|
204
|
+
"bid_price_cents": bid_price,
|
|
205
|
+
"currency": currency,
|
|
206
|
+
}
|
|
207
|
+
for opt_field in ["estimated_contact_seconds", "buyer_reference", "reject_reason"]:
|
|
208
|
+
if opt_field in arguments:
|
|
209
|
+
payload[opt_field] = arguments[opt_field]
|
|
210
|
+
receiver_id = "platform" # bids go to the platform, not a buyer
|
|
211
|
+
envelope = _make_envelope("bid", client.sender_id, receiver_id, payload)
|
|
212
|
+
result = client.post("/v1/lcp/bids", envelope)
|
|
213
|
+
return _tool_result(result)
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
return _error_result(f"Unknown tool: {name}", code="LCP-006")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ─── MCP SDK compatibility ───────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
# MCP 1.x exposed decorator methods on Server; MCP 2.x accepts callbacks in
|
|
222
|
+
# the constructor. Keep the adapter compatible with both supported API shapes
|
|
223
|
+
# while the dependency range transitions to the 2.x server implementation.
|
|
224
|
+
if hasattr(Server, "list_tools"):
|
|
225
|
+
legacy_app = Server("lcp-mcp-server")
|
|
226
|
+
list_tools = legacy_app.list_tools()(_list_tools)
|
|
227
|
+
call_tool = legacy_app.call_tool()(_call_tool)
|
|
228
|
+
app = legacy_app
|
|
229
|
+
else:
|
|
230
|
+
async def list_tools(_context: Any, _params: Any) -> types.ListToolsResult:
|
|
231
|
+
return types.ListToolsResult(tools=await _list_tools())
|
|
232
|
+
|
|
233
|
+
async def call_tool(_context: Any, params: types.CallToolRequestParams) -> types.CallToolResult:
|
|
234
|
+
content = await _call_tool(params.name, params.arguments or {})
|
|
235
|
+
return types.CallToolResult(content=content)
|
|
236
|
+
|
|
237
|
+
app = Server(
|
|
238
|
+
"lcp-mcp-server",
|
|
239
|
+
on_list_tools=list_tools,
|
|
240
|
+
on_call_tool=call_tool,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ─── Entrypoint ──────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
async def _run() -> None:
|
|
247
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
248
|
+
await app.run(read_stream, write_stream, app.create_initialization_options())
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def main() -> None:
|
|
252
|
+
"""Entrypoint for the lcp-mcp-server command."""
|
|
253
|
+
import asyncio
|
|
254
|
+
asyncio.run(_run())
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lcp-mcp-server"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "LCP (Lead Context Protocol) MCP server — thin adapter wrapping LCP REST endpoints as agent tools."
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mcp>=1.28.1,<2.1.0",
|
|
9
|
+
"lcp-sdk>=1.0.0",
|
|
10
|
+
"httpx>=0.27.0",
|
|
11
|
+
"jsonschema>=4.20.0",
|
|
12
|
+
"referencing>=0.35.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
lcp-mcp-server = "lcp_mcp_server.server:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["lcp_mcp_server"]
|