Skip to content

Commit e6bc448

Browse files
committed
MCPToolkit that accepts an MCP ClientSession
1 parent c0fc714 commit e6bc448

File tree

7 files changed

+173
-3
lines changed

7 files changed

+173
-3
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
uses: astral-sh/setup-uv@v2
2121
with:
2222
enable-cache: true
23-
#cache-dependency-glob: "uv.lock"
23+
cache-dependency-glob: "uv.lock"
2424
- name: Set up Python 3.12
2525
uses: actions/setup-python@v5
2626
with:

.vscode/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"[python]": {
3+
"editor.formatOnSave": true,
4+
"editor.codeActionsOnSave": {
5+
"source.organizeImports": "explicit"
6+
},
7+
"editor.defaultFormatter": "charliermarsh.ruff"
8+
}
9+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Andrew Wason
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.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# langchain-mcp
22

3-
Placeholder
3+
[Model Context Protocol](https://modelcontextprotocol.io) tool calling support in LangChain.

pyproject.toml

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,62 @@ authors = [
77
{ name = "Andrew Wason", email = "[email protected]" }
88
]
99
requires-python = ">=3.10"
10-
dependencies = []
10+
dependencies = [
11+
"langchain-core~=0.3.21",
12+
"mcp~=1.0.0",
13+
"pydantic>=2.10.2",
14+
]
15+
classifiers = [
16+
"License :: OSI Approved :: MIT License",
17+
]
1118

1219
[build-system]
1320
requires = ["hatchling"]
1421
build-backend = "hatchling.build"
22+
23+
[dependency-groups]
24+
dev = [
25+
"langchain-tests~=0.3.4",
26+
"pytest~=8.3.3",
27+
"pytest-asyncio~=0.24.0",
28+
"pytest-socket~=0.7.0",
29+
"ruff~=0.8.0",
30+
]
31+
32+
[project.urls]
33+
Repository = "https://github.com/rectalogic/langchain-mcp"
34+
Issues = "https://github.com/rectalogic/langchain-mcp/issues"
35+
Related = "https://modelcontextprotocol.io/"
36+
37+
[tool.ruff]
38+
target-version = "py310"
39+
line-length = 120
40+
41+
[tool.ruff.format]
42+
docstring-code-format = true
43+
44+
[tool.ruff.lint]
45+
select = [
46+
# flake8-2020
47+
"YTT",
48+
# flake8-bandit
49+
"S",
50+
# flake8-bugbear
51+
"B",
52+
# flake8-builtins
53+
"A",
54+
# Pyflakes
55+
"F",
56+
# Pycodestyle
57+
"E",
58+
"W",
59+
# isort
60+
"I",
61+
# flake8-no-pep420
62+
"INP",
63+
# pyupgrade
64+
"UP",
65+
]
66+
67+
[tool.ruff.lint.per-file-ignores]
68+
"tests/*" = ["S", "INP001"]

src/langchain_mcp/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright (C) 2024 Andrew Wason
2+
# SPDX-License-Identifier: MIT
3+
4+
from .toolkit import MCPToolkit # noqa: F401

src/langchain_mcp/toolkit.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (C) 2024 Andrew Wason
2+
# SPDX-License-Identifier: MIT
3+
4+
import typing as t
5+
from collections.abc import Callable
6+
7+
import pydantic
8+
import pydantic_core
9+
from langchain_core.tools.base import BaseTool, BaseToolkit, InjectedToolArg, ToolException
10+
from mcp import ClientSession
11+
12+
13+
class MCPToolkit(BaseToolkit):
14+
"""
15+
MCP server toolkit
16+
"""
17+
18+
session: ClientSession
19+
"""The MCP session used to obtain the tools"""
20+
21+
_initialized: bool = False
22+
23+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
24+
25+
async def get_tools(self) -> list[BaseTool]:
26+
if not self._initialized:
27+
await self.session.initialize()
28+
self._initialized = True
29+
30+
return [
31+
MCPTool(
32+
session=self.session,
33+
name=tool.name,
34+
description=tool.description,
35+
args_schema=create_schema_model(tool.inputSchema),
36+
)
37+
# list_tools returns a PaginatedResult, but I don't see a way to pass the cursor to retrieve more tools
38+
for tool in (await self.session.list_tools()).tools
39+
]
40+
41+
42+
def create_schema_model(schema: dict[str, t.Any]) -> type[pydantic.BaseModel]:
43+
# Create a new model class that returns our JSON schema.
44+
# LangChain requires a BaseModel class.
45+
class Schema(pydantic.BaseModel):
46+
model_config = pydantic.ConfigDict(extra="allow", arbitrary_types_allowed=True)
47+
48+
@classmethod
49+
def model_json_schema(
50+
cls,
51+
by_alias: bool = True,
52+
ref_template: str = pydantic.json_schema.DEFAULT_REF_TEMPLATE,
53+
schema_generator: type[pydantic.json_schema.GenerateJsonSchema] = pydantic.json_schema.GenerateJsonSchema,
54+
mode: pydantic.json_schema.JsonSchemaMode = "validation",
55+
) -> dict[str, t.Any]:
56+
return schema
57+
58+
return Schema
59+
60+
61+
class MCPTool(BaseTool):
62+
"""
63+
MCP server tool
64+
"""
65+
66+
session: ClientSession
67+
68+
handle_tool_error: bool | str | Callable[[ToolException], str] | None = True
69+
70+
def _run(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
71+
raise NotImplementedError("Must invoke tool asynchronously")
72+
73+
async def _arun(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
74+
result = await self.session.call_tool(self.name, arguments=kwargs)
75+
content = pydantic_core.to_json(result.content).decode()
76+
if result.isError:
77+
raise ToolException(content)
78+
return content
79+
80+
@property
81+
def tool_call_schema(self) -> type[pydantic.BaseModel]:
82+
return self.args_schema

0 commit comments

Comments
 (0)