langchain-postbye 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.
- langchain_postbye-1.0.0/LICENSE +21 -0
- langchain_postbye-1.0.0/PKG-INFO +93 -0
- langchain_postbye-1.0.0/README.md +69 -0
- langchain_postbye-1.0.0/pyproject.toml +40 -0
- langchain_postbye-1.0.0/src/langchain_postbye/__init__.py +19 -0
- langchain_postbye-1.0.0/src/langchain_postbye/client.py +95 -0
- langchain_postbye-1.0.0/src/langchain_postbye/toolkit.py +104 -0
- langchain_postbye-1.0.0/tests/test_smoke.py +33 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Postbye Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-postbye
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LangChain tools for Postbye's public cancellation-guide MCP server: search companies, get cancellation guides and cancellation letters.
|
|
5
|
+
Project-URL: Homepage, https://postbye.com/mcp-server
|
|
6
|
+
Project-URL: Documentation, https://postbye.com/mcp-server
|
|
7
|
+
Project-URL: Repository, https://github.com/postbye-team/postbye-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/postbye-team/postbye-mcp/issues
|
|
9
|
+
Author-email: Postbye Team <support@postbye.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,cancel-subscription,cancellation-letter,langchain,mcp,model-context-protocol,postbye
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: httpx>=0.25
|
|
19
|
+
Requires-Dist: langchain-core>=0.3
|
|
20
|
+
Requires-Dist: pydantic>=2
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
Postbye (postbye.com) cancels subscriptions for you by mailing a formal cancellation letter, tracked until it is delivered.
|
|
26
|
+
|
|
27
|
+
# langchain-postbye
|
|
28
|
+
|
|
29
|
+
LangChain tools for Postbye's public, read-only MCP server at <https://postbye.com/mcp> (Streamable HTTP, no API key, no account). Give your agent the ability to find a company's cancellation rules, mailing address and a ready-to-send cancellation letter.
|
|
30
|
+
|
|
31
|
+
The package calls the server with plain JSON-RPC over HTTP, so it needs no MCP client library. Tools support both sync and async use.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install langchain-postbye
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from langchain_postbye import PostbyeToolkit
|
|
43
|
+
|
|
44
|
+
tools = PostbyeToolkit().get_tools() # or: from langchain_postbye import get_tools
|
|
45
|
+
|
|
46
|
+
search = next(t for t in tools if t.name == "postbye_search_companies")
|
|
47
|
+
print(search.invoke({"query": "planet fitness", "limit": 3}))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### With a LangGraph agent
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from langchain_postbye import get_tools
|
|
54
|
+
from langgraph.prebuilt import create_react_agent
|
|
55
|
+
|
|
56
|
+
agent = create_react_agent("anthropic:claude-sonnet-4-5", get_tools())
|
|
57
|
+
result = agent.invoke(
|
|
58
|
+
{"messages": [("user", "How do I cancel my Planet Fitness membership? Draft the letter.")]}
|
|
59
|
+
)
|
|
60
|
+
print(result["messages"][-1].content)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
| LangChain tool | MCP tool | What it does |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `postbye_search_companies` | `search_companies` | Find a company by name; returns its slug, category and how it takes cancellations. |
|
|
68
|
+
| `postbye_get_cancellation_guide` | `get_cancellation_guide` | Notice period, traps to check, billing-partner notes, sources and mailing address(es) for one company. |
|
|
69
|
+
| `postbye_get_cancellation_letter` | `get_cancellation_letter` | A plain-text cancellation letter plus where to send it. Optional `customer_name` and `account_number`. Nothing is stored. |
|
|
70
|
+
| `postbye_list_categories` | `list_categories` | Guide categories with the number of guides in each. |
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
PostbyeToolkit(url="https://postbye.com/mcp", timeout=30.0).get_tools()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Tests
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install -e ".[test]"
|
|
82
|
+
pytest # live smoke tests: tools/list must return the 4 tools
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Links
|
|
86
|
+
|
|
87
|
+
- Homepage and docs: <https://postbye.com/mcp-server>
|
|
88
|
+
- Repository: <https://github.com/postbye-team/postbye-mcp>
|
|
89
|
+
- Support: support@postbye.com
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT. Author: Postbye Team.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Postbye (postbye.com) cancels subscriptions for you by mailing a formal cancellation letter, tracked until it is delivered.
|
|
2
|
+
|
|
3
|
+
# langchain-postbye
|
|
4
|
+
|
|
5
|
+
LangChain tools for Postbye's public, read-only MCP server at <https://postbye.com/mcp> (Streamable HTTP, no API key, no account). Give your agent the ability to find a company's cancellation rules, mailing address and a ready-to-send cancellation letter.
|
|
6
|
+
|
|
7
|
+
The package calls the server with plain JSON-RPC over HTTP, so it needs no MCP client library. Tools support both sync and async use.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install langchain-postbye
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from langchain_postbye import PostbyeToolkit
|
|
19
|
+
|
|
20
|
+
tools = PostbyeToolkit().get_tools() # or: from langchain_postbye import get_tools
|
|
21
|
+
|
|
22
|
+
search = next(t for t in tools if t.name == "postbye_search_companies")
|
|
23
|
+
print(search.invoke({"query": "planet fitness", "limit": 3}))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### With a LangGraph agent
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from langchain_postbye import get_tools
|
|
30
|
+
from langgraph.prebuilt import create_react_agent
|
|
31
|
+
|
|
32
|
+
agent = create_react_agent("anthropic:claude-sonnet-4-5", get_tools())
|
|
33
|
+
result = agent.invoke(
|
|
34
|
+
{"messages": [("user", "How do I cancel my Planet Fitness membership? Draft the letter.")]}
|
|
35
|
+
)
|
|
36
|
+
print(result["messages"][-1].content)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| LangChain tool | MCP tool | What it does |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| `postbye_search_companies` | `search_companies` | Find a company by name; returns its slug, category and how it takes cancellations. |
|
|
44
|
+
| `postbye_get_cancellation_guide` | `get_cancellation_guide` | Notice period, traps to check, billing-partner notes, sources and mailing address(es) for one company. |
|
|
45
|
+
| `postbye_get_cancellation_letter` | `get_cancellation_letter` | A plain-text cancellation letter plus where to send it. Optional `customer_name` and `account_number`. Nothing is stored. |
|
|
46
|
+
| `postbye_list_categories` | `list_categories` | Guide categories with the number of guides in each. |
|
|
47
|
+
|
|
48
|
+
## Options
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
PostbyeToolkit(url="https://postbye.com/mcp", timeout=30.0).get_tools()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Tests
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install -e ".[test]"
|
|
58
|
+
pytest # live smoke tests: tools/list must return the 4 tools
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Links
|
|
62
|
+
|
|
63
|
+
- Homepage and docs: <https://postbye.com/mcp-server>
|
|
64
|
+
- Repository: <https://github.com/postbye-team/postbye-mcp>
|
|
65
|
+
- Support: support@postbye.com
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT. Author: Postbye Team.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-postbye"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "LangChain tools for Postbye's public cancellation-guide MCP server: search companies, get cancellation guides and cancellation letters."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "Postbye Team", email = "support@postbye.com" }]
|
|
14
|
+
keywords = ["langchain", "mcp", "model-context-protocol", "postbye", "cancel-subscription", "cancellation-letter", "agents"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"langchain-core>=0.3",
|
|
23
|
+
"httpx>=0.25",
|
|
24
|
+
"pydantic>=2",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = ["pytest>=7"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://postbye.com/mcp-server"
|
|
32
|
+
Documentation = "https://postbye.com/mcp-server"
|
|
33
|
+
Repository = "https://github.com/postbye-team/postbye-mcp"
|
|
34
|
+
Issues = "https://github.com/postbye-team/postbye-mcp/issues"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/langchain_postbye"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""LangChain tools for Postbye's public, read-only cancellation-guide MCP server.
|
|
2
|
+
|
|
3
|
+
Postbye (postbye.com) cancels subscriptions for you by mailing a formal
|
|
4
|
+
cancellation letter, tracked until it is delivered.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from langchain_postbye.client import MCP_URL, PostbyeClient, PostbyeError
|
|
8
|
+
from langchain_postbye.toolkit import PostbyeToolkit, get_tools
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MCP_URL",
|
|
14
|
+
"PostbyeClient",
|
|
15
|
+
"PostbyeError",
|
|
16
|
+
"PostbyeToolkit",
|
|
17
|
+
"get_tools",
|
|
18
|
+
"__version__",
|
|
19
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Minimal JSON-RPC client for the Postbye MCP server (Streamable HTTP, no API key)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
MCP_URL = "https://postbye.com/mcp"
|
|
12
|
+
PROTOCOL_VERSION = "2025-03-26"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PostbyeError(RuntimeError):
|
|
16
|
+
"""Raised when the Postbye MCP server returns a JSON-RPC error or a tool error."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_body(response: httpx.Response) -> Dict[str, Any]:
|
|
20
|
+
"""Return the JSON-RPC message from a JSON or a single-event SSE response."""
|
|
21
|
+
content_type = response.headers.get("content-type", "")
|
|
22
|
+
if "text/event-stream" in content_type:
|
|
23
|
+
for line in response.text.splitlines():
|
|
24
|
+
if line.startswith("data:"):
|
|
25
|
+
data = line[5:].strip()
|
|
26
|
+
if data:
|
|
27
|
+
return json.loads(data)
|
|
28
|
+
raise PostbyeError("Empty event stream from the Postbye MCP server")
|
|
29
|
+
return response.json()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _unwrap(message: Dict[str, Any]) -> Any:
|
|
33
|
+
if "error" in message:
|
|
34
|
+
err = message["error"]
|
|
35
|
+
raise PostbyeError(f"{err.get('message', 'MCP error')} (code {err.get('code')})")
|
|
36
|
+
return message.get("result")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tool_text(result: Dict[str, Any]) -> str:
|
|
40
|
+
text = "\n".join(
|
|
41
|
+
block.get("text", "") for block in result.get("content", []) if block.get("type") == "text"
|
|
42
|
+
).strip()
|
|
43
|
+
if result.get("isError"):
|
|
44
|
+
raise PostbyeError(text or "Postbye tool call failed")
|
|
45
|
+
return text
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PostbyeClient:
|
|
49
|
+
"""Calls tools on https://postbye.com/mcp with plain JSON-RPC over HTTP.
|
|
50
|
+
|
|
51
|
+
The server is stateless and read-only, so no session or authentication is needed.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, url: str = MCP_URL, timeout: float = 30.0) -> None:
|
|
55
|
+
self.url = url
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
self._ids = itertools.count(1)
|
|
58
|
+
self._headers = {
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
"accept": "application/json, text/event-stream",
|
|
61
|
+
"mcp-protocol-version": PROTOCOL_VERSION,
|
|
62
|
+
"user-agent": "langchain-postbye/1.0.0",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def _payload(self, method: str, params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
66
|
+
payload: Dict[str, Any] = {"jsonrpc": "2.0", "id": next(self._ids), "method": method}
|
|
67
|
+
if params is not None:
|
|
68
|
+
payload["params"] = params
|
|
69
|
+
return payload
|
|
70
|
+
|
|
71
|
+
def _request(self, method: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
72
|
+
with httpx.Client(timeout=self.timeout, headers=self._headers) as http:
|
|
73
|
+
response = http.post(self.url, json=self._payload(method, params))
|
|
74
|
+
response.raise_for_status()
|
|
75
|
+
return _unwrap(_parse_body(response))
|
|
76
|
+
|
|
77
|
+
async def _arequest(self, method: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
78
|
+
async with httpx.AsyncClient(timeout=self.timeout, headers=self._headers) as http:
|
|
79
|
+
response = await http.post(self.url, json=self._payload(method, params))
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
return _unwrap(_parse_body(response))
|
|
82
|
+
|
|
83
|
+
def list_tools(self) -> List[Dict[str, Any]]:
|
|
84
|
+
return self._request("tools/list")["tools"]
|
|
85
|
+
|
|
86
|
+
async def alist_tools(self) -> List[Dict[str, Any]]:
|
|
87
|
+
return (await self._arequest("tools/list"))["tools"]
|
|
88
|
+
|
|
89
|
+
def call_tool(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
|
|
90
|
+
return _tool_text(self._request("tools/call", {"name": name, "arguments": arguments or {}}))
|
|
91
|
+
|
|
92
|
+
async def acall_tool(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> str:
|
|
93
|
+
return _tool_text(
|
|
94
|
+
await self._arequest("tools/call", {"name": name, "arguments": arguments or {}})
|
|
95
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""LangChain tool definitions for the four Postbye MCP tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from langchain_core.tools import BaseTool, BaseToolkit, StructuredTool
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
from langchain_postbye.client import MCP_URL, PostbyeClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SearchCompaniesInput(BaseModel):
|
|
14
|
+
query: str = Field(min_length=1, max_length=100, description="Company or service name, or part of it.")
|
|
15
|
+
limit: int = Field(default=5, ge=1, le=10, description="Maximum number of matches (1 to 10).")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CompanySlugInput(BaseModel):
|
|
19
|
+
slug: str = Field(min_length=1, max_length=100, description='Company slug from postbye_search_companies, for example "netflix".')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CancellationLetterInput(BaseModel):
|
|
23
|
+
slug: str = Field(min_length=1, max_length=100, description="Company slug from postbye_search_companies.")
|
|
24
|
+
customer_name: Optional[str] = Field(default=None, max_length=100, description="Your full name as it appears on the account (optional).")
|
|
25
|
+
account_number: Optional[str] = Field(default=None, max_length=64, description="Account, membership or reference number (optional).")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NoInput(BaseModel):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# (langchain tool name, MCP tool name, args schema, description)
|
|
33
|
+
_TOOLS = [
|
|
34
|
+
(
|
|
35
|
+
"postbye_search_companies",
|
|
36
|
+
"search_companies",
|
|
37
|
+
SearchCompaniesInput,
|
|
38
|
+
"Find companies by name (for example \"netflix\" or \"planet fitness\") among the subscriptions and "
|
|
39
|
+
"memberships Postbye has a cancellation guide for. Returns each match's slug, category, how the company "
|
|
40
|
+
"takes cancellations and the guide URL. Use the slug with postbye_get_cancellation_guide.",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
"postbye_get_cancellation_guide",
|
|
44
|
+
"get_cancellation_guide",
|
|
45
|
+
CompanySlugInput,
|
|
46
|
+
"How to cancel one company's subscription or membership: how it takes cancellations, the notice period, "
|
|
47
|
+
"the traps to check first, billing-partner notes, sources, when the facts were verified, the mailing "
|
|
48
|
+
"address(es) on file and the guide and address page URLs.",
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
"postbye_get_cancellation_letter",
|
|
52
|
+
"get_cancellation_letter",
|
|
53
|
+
CancellationLetterInput,
|
|
54
|
+
"A plain-text cancellation letter for one company, built from the same template as Postbye's free letter "
|
|
55
|
+
"tool, plus where to send it. Fields not supplied stay as [bracketed placeholders] to fill in. Nothing is stored.",
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
"postbye_list_categories",
|
|
59
|
+
"list_categories",
|
|
60
|
+
NoInput,
|
|
61
|
+
"The category hubs of the Postbye cancellation guides (streaming, gyms, phone and internet, insurance and "
|
|
62
|
+
"more) with the number of guides in each and the hub URL.",
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_tool(client: PostbyeClient, name: str, mcp_name: str, schema: type, description: str) -> StructuredTool:
|
|
68
|
+
def _run(**kwargs: object) -> str:
|
|
69
|
+
return client.call_tool(mcp_name, {k: v for k, v in kwargs.items() if v is not None})
|
|
70
|
+
|
|
71
|
+
async def _arun(**kwargs: object) -> str:
|
|
72
|
+
return await client.acall_tool(mcp_name, {k: v for k, v in kwargs.items() if v is not None})
|
|
73
|
+
|
|
74
|
+
return StructuredTool(
|
|
75
|
+
name=name,
|
|
76
|
+
description=description,
|
|
77
|
+
args_schema=schema,
|
|
78
|
+
func=_run,
|
|
79
|
+
coroutine=_arun,
|
|
80
|
+
metadata={"mcp_tool": mcp_name, "source": MCP_URL},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PostbyeToolkit(BaseToolkit):
|
|
85
|
+
"""Toolkit exposing Postbye's four read-only cancellation tools.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> from langchain_postbye import PostbyeToolkit
|
|
89
|
+
>>> tools = PostbyeToolkit().get_tools()
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
93
|
+
|
|
94
|
+
url: str = MCP_URL
|
|
95
|
+
timeout: float = 30.0
|
|
96
|
+
|
|
97
|
+
def get_tools(self) -> List[BaseTool]:
|
|
98
|
+
client = PostbyeClient(url=self.url, timeout=self.timeout)
|
|
99
|
+
return [_make_tool(client, *spec) for spec in _TOOLS]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_tools(url: str = MCP_URL, timeout: float = 30.0) -> List[BaseTool]:
|
|
103
|
+
"""Return the four Postbye tools as LangChain tools."""
|
|
104
|
+
return PostbyeToolkit(url=url, timeout=timeout).get_tools()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Live smoke tests against https://postbye.com/mcp."""
|
|
2
|
+
|
|
3
|
+
from langchain_postbye import PostbyeClient, PostbyeToolkit, get_tools
|
|
4
|
+
|
|
5
|
+
EXPECTED = {
|
|
6
|
+
"get_cancellation_guide",
|
|
7
|
+
"get_cancellation_letter",
|
|
8
|
+
"list_categories",
|
|
9
|
+
"search_companies",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_tools_list_returns_four_tools():
|
|
14
|
+
tools = PostbyeClient().list_tools()
|
|
15
|
+
assert {t["name"] for t in tools} == EXPECTED
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_langchain_tools_match_server():
|
|
19
|
+
tools = get_tools()
|
|
20
|
+
assert len(tools) == 4
|
|
21
|
+
assert {t.metadata["mcp_tool"] for t in tools} == EXPECTED
|
|
22
|
+
assert len(PostbyeToolkit().get_tools()) == 4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_search_tool_invokes_live():
|
|
26
|
+
search = next(t for t in get_tools() if t.name == "postbye_search_companies")
|
|
27
|
+
out = search.invoke({"query": "netflix", "limit": 1})
|
|
28
|
+
assert "netflix" in out.lower()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_list_categories_invokes_live():
|
|
32
|
+
cats = next(t for t in get_tools() if t.name == "postbye_list_categories")
|
|
33
|
+
assert cats.invoke({}).strip()
|