langfun 0.1.2.dev202510170805__py3-none-any.whl → 0.1.2.dev202510190804__py3-none-any.whl
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.
Potentially problematic release.
This version of langfun might be problematic. Click here for more details.
- langfun/__init__.py +1 -1
- langfun/core/mcp/__init__.py +10 -0
- langfun/core/mcp/client.py +122 -0
- langfun/core/mcp/client_test.py +98 -0
- langfun/core/mcp/session.py +176 -0
- langfun/core/mcp/testing/simple_mcp_client.py +33 -0
- langfun/core/mcp/testing/simple_mcp_server.py +33 -0
- langfun/core/mcp/tool.py +125 -0
- {langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/METADATA +5 -3
- {langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/RECORD +13 -6
- {langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/top_level.txt +0 -0
langfun/__init__.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Langfun MCP support."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=g-importing-member
|
|
4
|
+
|
|
5
|
+
from langfun.core.mcp.client import McpClient
|
|
6
|
+
from langfun.core.mcp.session import McpSession
|
|
7
|
+
from langfun.core.mcp.tool import McpTool
|
|
8
|
+
from langfun.core.mcp.tool import McpToolInput
|
|
9
|
+
|
|
10
|
+
# pylint: enable=g-importing-member
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""MCP client."""
|
|
15
|
+
|
|
16
|
+
import abc
|
|
17
|
+
from typing import Annotated, Type
|
|
18
|
+
|
|
19
|
+
from langfun.core.mcp import session as mcp_session
|
|
20
|
+
from langfun.core.mcp import tool as mcp_tool
|
|
21
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
22
|
+
import pyglove as pg
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class McpClient(pg.Object):
|
|
26
|
+
"""Base class for MCP client.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
|
|
32
|
+
def tool_use():
|
|
33
|
+
client = lf.mcp.McpClient.from_command('<MCP_CMD>', ['<ARG1>', 'ARG2'])
|
|
34
|
+
tools = client.list_tools()
|
|
35
|
+
tool_cls = tools['<TOOL_NAME>']
|
|
36
|
+
|
|
37
|
+
# Print the python definition of the tool.
|
|
38
|
+
print(tool_cls.python_definition())
|
|
39
|
+
|
|
40
|
+
with client.session() as session:
|
|
41
|
+
return tool_cls(x=1, y=2)(session)
|
|
42
|
+
|
|
43
|
+
async def tool_use_async_version():
|
|
44
|
+
client = lf.mcp.McpClient.from_url('http://localhost:8000/mcp')
|
|
45
|
+
tools = client.list_tools()
|
|
46
|
+
tool_cls = tools['<TOOL_NAME>']
|
|
47
|
+
|
|
48
|
+
# Print the python definition of the tool.
|
|
49
|
+
print(tool_cls.python_definition())
|
|
50
|
+
|
|
51
|
+
async with client.session() as session:
|
|
52
|
+
return await tool_cls(x=1, y=2).acall(session)
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def _on_bound(self):
|
|
57
|
+
super()._on_bound()
|
|
58
|
+
self._tools = None
|
|
59
|
+
|
|
60
|
+
def list_tools(
|
|
61
|
+
self, refresh: bool = False
|
|
62
|
+
) -> dict[str, Type[mcp_tool.McpTool]]:
|
|
63
|
+
"""Lists all MCP tools."""
|
|
64
|
+
if self._tools is None or refresh:
|
|
65
|
+
with self.session() as session:
|
|
66
|
+
self._tools = session.list_tools()
|
|
67
|
+
return self._tools
|
|
68
|
+
|
|
69
|
+
@abc.abstractmethod
|
|
70
|
+
def session(self) -> mcp_session.McpSession:
|
|
71
|
+
"""Creates a MCP session."""
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_command(cls, command: str, args: list[str]) -> 'McpClient':
|
|
75
|
+
"""Creates a MCP client from a tool."""
|
|
76
|
+
return _StdioMcpClient(command=command, args=args)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_url(
|
|
80
|
+
cls,
|
|
81
|
+
url: str,
|
|
82
|
+
headers: dict[str, str] | None = None
|
|
83
|
+
) -> 'McpClient':
|
|
84
|
+
"""Creates a MCP client from a URL."""
|
|
85
|
+
return _HttpMcpClient(url=url, headers=headers or {})
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_fastmcp(cls, fastmcp: fastmcp_lib.FastMCP) -> 'McpClient':
|
|
89
|
+
"""Creates a MCP client from a MCP server."""
|
|
90
|
+
return _InMemoryFastMcpClient(fastmcp=fastmcp)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _StdioMcpClient(McpClient):
|
|
94
|
+
"""Stdio-based MCP client."""
|
|
95
|
+
|
|
96
|
+
command: Annotated[str, 'Command to execute.']
|
|
97
|
+
args: Annotated[list[str], 'Arguments to pass to the command.']
|
|
98
|
+
|
|
99
|
+
def session(self) -> mcp_session.McpSession:
|
|
100
|
+
"""Creates a MCP session."""
|
|
101
|
+
return mcp_session.McpSession.from_command(self.command, self.args)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _HttpMcpClient(McpClient):
|
|
105
|
+
"""Server-Sent Events (SSE)/Streamable HTTP-based MCP client."""
|
|
106
|
+
|
|
107
|
+
url: Annotated[str, 'URL to connect to.']
|
|
108
|
+
headers: Annotated[dict[str, str], 'Headers to send with the request.'] = {}
|
|
109
|
+
|
|
110
|
+
def session(self) -> mcp_session.McpSession:
|
|
111
|
+
"""Creates a MCP session."""
|
|
112
|
+
return mcp_session.McpSession.from_url(self.url, self.headers)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _InMemoryFastMcpClient(McpClient):
|
|
116
|
+
"""In-memory MCP client."""
|
|
117
|
+
|
|
118
|
+
fastmcp: Annotated[fastmcp_lib.FastMCP, 'MCP server to connect to.']
|
|
119
|
+
|
|
120
|
+
def session(self) -> mcp_session.McpSession:
|
|
121
|
+
"""Creates a MCP session."""
|
|
122
|
+
return mcp_session.McpSession.from_fastmcp(self.fastmcp)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Tests for MCP client."""
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import unittest
|
|
18
|
+
from langfun.core import async_support
|
|
19
|
+
from langfun.core import mcp as lf_mcp
|
|
20
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
21
|
+
|
|
22
|
+
mcp = fastmcp_lib.FastMCP(host='0.0.0.0', port=1234)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
async def add(a: int, b: int) -> int:
|
|
27
|
+
"""Adds two integers and returns their sum.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
a: The first integer.
|
|
31
|
+
b: The second integer.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The sum of the two integers.
|
|
35
|
+
"""
|
|
36
|
+
return a + b
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class McpTest(unittest.TestCase):
|
|
40
|
+
|
|
41
|
+
def test_sync_usages(self):
|
|
42
|
+
client = lf_mcp.McpClient.from_fastmcp(mcp)
|
|
43
|
+
tools = client.list_tools()
|
|
44
|
+
self.assertEqual(len(tools), 1)
|
|
45
|
+
tool_cls = tools['add']
|
|
46
|
+
print(tool_cls.python_definition())
|
|
47
|
+
self.assertEqual(
|
|
48
|
+
tool_cls.python_definition(),
|
|
49
|
+
inspect.cleandoc(
|
|
50
|
+
'''
|
|
51
|
+
Add
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
class Add:
|
|
55
|
+
"""Adds two integers and returns their sum.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
a: The first integer.
|
|
59
|
+
b: The second integer.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The sum of the two integers.
|
|
63
|
+
"""
|
|
64
|
+
a: int
|
|
65
|
+
b: int
|
|
66
|
+
```
|
|
67
|
+
'''
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
self.assertEqual(repr(tool_cls), '<tool-class \'Add\'>')
|
|
71
|
+
self.assertEqual(tool_cls.__name__, 'Add')
|
|
72
|
+
self.assertEqual(tool_cls.TOOL_NAME, 'add')
|
|
73
|
+
self.assertEqual(tool_cls(a=1, b=2).input_parameters(), {'a': 1, 'b': 2})
|
|
74
|
+
with client.session() as session:
|
|
75
|
+
self.assertEqual(
|
|
76
|
+
tool_cls(a=1, b=2)(session), 3
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def test_async_usages(self):
|
|
80
|
+
async def _test():
|
|
81
|
+
client = lf_mcp.McpClient.from_fastmcp(mcp)
|
|
82
|
+
tools = client.list_tools()
|
|
83
|
+
self.assertEqual(len(tools), 1)
|
|
84
|
+
tool_cls = tools['add']
|
|
85
|
+
self.assertEqual(tool_cls.__name__, 'Add')
|
|
86
|
+
self.assertEqual(tool_cls.TOOL_NAME, 'add')
|
|
87
|
+
async with client.session() as session:
|
|
88
|
+
self.assertEqual(
|
|
89
|
+
(await tool_cls(a=1, b=2).acall(
|
|
90
|
+
session, returns_call_result=True))
|
|
91
|
+
.structuredContent['result'],
|
|
92
|
+
3
|
|
93
|
+
)
|
|
94
|
+
async_support.invoke_sync(_test)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == '__main__':
|
|
98
|
+
unittest.main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""MCP session."""
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
from typing import Any, Type
|
|
18
|
+
import anyio
|
|
19
|
+
from langfun.core import async_support
|
|
20
|
+
from langfun.core.mcp import tool as mcp_tool
|
|
21
|
+
import mcp
|
|
22
|
+
from mcp.client import sse
|
|
23
|
+
from mcp.client import streamable_http
|
|
24
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
25
|
+
from mcp.shared import memory
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpSession:
|
|
29
|
+
"""Langfun's MCP session.
|
|
30
|
+
|
|
31
|
+
Compared to the standard mcp.ClientSession, Langfun's MCP session could be
|
|
32
|
+
used both synchronously and asynchronously.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, stream) -> None:
|
|
36
|
+
self._stream = stream
|
|
37
|
+
self._session = None
|
|
38
|
+
self._session_exit_stack = None
|
|
39
|
+
self._in_session = False
|
|
40
|
+
|
|
41
|
+
# For supporting sync context manager.
|
|
42
|
+
self._sync_context_manager_exit_stack = None
|
|
43
|
+
|
|
44
|
+
def __enter__(self) -> 'McpSession':
|
|
45
|
+
exit_stack = contextlib.ExitStack()
|
|
46
|
+
exit_stack.enter_context(async_support.sync_context_manager(self))
|
|
47
|
+
self._sync_context_manager_exit_stack = exit_stack
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
51
|
+
assert self._sync_context_manager_exit_stack is not None
|
|
52
|
+
self._sync_context_manager_exit_stack.close()
|
|
53
|
+
|
|
54
|
+
async def __aenter__(self) -> 'McpSession':
|
|
55
|
+
assert self._session_exit_stack is None, 'Session cannot be re-entered.'
|
|
56
|
+
|
|
57
|
+
self._session_exit_stack = contextlib.AsyncExitStack()
|
|
58
|
+
stream_output = await self._session_exit_stack.enter_async_context(
|
|
59
|
+
self._stream
|
|
60
|
+
)
|
|
61
|
+
assert isinstance(stream_output, tuple) and len(stream_output) in (2, 3)
|
|
62
|
+
read, write = stream_output[:2]
|
|
63
|
+
self._session = mcp.ClientSession(read, write)
|
|
64
|
+
await self._session_exit_stack.enter_async_context(self._session)
|
|
65
|
+
await self._session.initialize()
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
69
|
+
del exc_type, exc_val, exc_tb
|
|
70
|
+
if self._session is None:
|
|
71
|
+
return
|
|
72
|
+
assert self._session_exit_stack is not None
|
|
73
|
+
await self._session_exit_stack.aclose()
|
|
74
|
+
self._session = None
|
|
75
|
+
|
|
76
|
+
def list_tools(self) -> dict[str, Type[mcp_tool.McpTool]]:
|
|
77
|
+
"""Lists all MCP tools synchronously."""
|
|
78
|
+
return async_support.invoke_sync(self.alist_tools)
|
|
79
|
+
|
|
80
|
+
async def alist_tools(self) -> dict[str, Type[mcp_tool.McpTool]]:
|
|
81
|
+
"""Lists all MCP tools asynchronously."""
|
|
82
|
+
assert self._session is not None, 'MCP session is not entered.'
|
|
83
|
+
return {
|
|
84
|
+
t.name: mcp_tool.McpTool.make_class(t)
|
|
85
|
+
for t in (await self._session.list_tools()).tools
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def call_tool(
|
|
89
|
+
self,
|
|
90
|
+
tool: mcp_tool.McpTool,
|
|
91
|
+
*,
|
|
92
|
+
returns_call_result: bool = False
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""Calls a MCP tool synchronously."""
|
|
95
|
+
return async_support.invoke_sync(
|
|
96
|
+
self.acall_tool,
|
|
97
|
+
tool,
|
|
98
|
+
returns_call_result=returns_call_result
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def acall_tool(
|
|
102
|
+
self,
|
|
103
|
+
tool: mcp_tool.McpTool,
|
|
104
|
+
*,
|
|
105
|
+
returns_call_result: bool = False
|
|
106
|
+
) -> Any:
|
|
107
|
+
"""Calls a MCP tool asynchronously."""
|
|
108
|
+
assert self._session is not None, 'MCP session is not entered.'
|
|
109
|
+
tool_call_result = await self._session.call_tool(
|
|
110
|
+
tool.TOOL_NAME, tool.input_parameters()
|
|
111
|
+
)
|
|
112
|
+
if returns_call_result:
|
|
113
|
+
return tool_call_result
|
|
114
|
+
if (
|
|
115
|
+
tool_call_result.structuredContent
|
|
116
|
+
and 'result' in tool_call_result.structuredContent
|
|
117
|
+
):
|
|
118
|
+
return tool_call_result.structuredContent['result']
|
|
119
|
+
return tool_call_result.content
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_command(
|
|
123
|
+
cls,
|
|
124
|
+
command: str,
|
|
125
|
+
args: list[str] | None = None
|
|
126
|
+
) -> 'McpSession':
|
|
127
|
+
"""Creates a MCP session from a command."""
|
|
128
|
+
return cls(
|
|
129
|
+
mcp.stdio_client(
|
|
130
|
+
mcp.StdioServerParameters(command=command, args=args or [])
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_url(
|
|
136
|
+
cls,
|
|
137
|
+
url: str,
|
|
138
|
+
headers: dict[str, str] | None = None
|
|
139
|
+
) -> 'McpSession':
|
|
140
|
+
"""Creates a MCP session from a URL."""
|
|
141
|
+
transport = url.removesuffix('/').split('/')[-1].lower()
|
|
142
|
+
if transport == 'mcp':
|
|
143
|
+
return cls(streamable_http.streamablehttp_client(url, headers or {}))
|
|
144
|
+
elif transport == 'sse':
|
|
145
|
+
return cls(sse.sse_client(url, headers or {}))
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(f'Unsupported transport: {transport}')
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_fastmcp(
|
|
151
|
+
cls,
|
|
152
|
+
fastmcp: fastmcp_lib.FastMCP
|
|
153
|
+
):
|
|
154
|
+
return cls(_client_streams_from_fastmcp(fastmcp))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@contextlib.asynccontextmanager
|
|
158
|
+
async def _client_streams_from_fastmcp(fastmcp: fastmcp_lib.FastMCP):
|
|
159
|
+
"""Creates client streams from a MCP server."""
|
|
160
|
+
server = fastmcp._mcp_server # pylint: disable=protected-access
|
|
161
|
+
async with memory.create_client_server_memory_streams(
|
|
162
|
+
) as (client_streams, server_streams):
|
|
163
|
+
client_read, client_write = client_streams
|
|
164
|
+
server_read, server_write = server_streams
|
|
165
|
+
|
|
166
|
+
# Create a cancel scope for the server task
|
|
167
|
+
async with anyio.create_task_group() as tg:
|
|
168
|
+
tg.start_soon(
|
|
169
|
+
lambda: server.run(
|
|
170
|
+
server_read,
|
|
171
|
+
server_write,
|
|
172
|
+
server.create_initialization_options(),
|
|
173
|
+
raise_exceptions=True,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
yield client_read, client_write
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""A simple MCP client for testing."""
|
|
15
|
+
|
|
16
|
+
from absl import app
|
|
17
|
+
from absl import flags
|
|
18
|
+
from langfun.core import mcp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_URL = flags.DEFINE_string(
|
|
22
|
+
'url',
|
|
23
|
+
'http://localhost:8000/mcp',
|
|
24
|
+
'URL of the MCP server.',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(_):
|
|
29
|
+
print(mcp.McpClient.from_url(url=_URL.value).list_tools())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
app.run(main)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Simple MCP server for testing."""
|
|
15
|
+
|
|
16
|
+
from absl import app as absl_app
|
|
17
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
18
|
+
|
|
19
|
+
mcp = fastmcp_lib.FastMCP(host='0.0.0.0', port=8000)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
async def add(a: int, b: int) -> int:
|
|
24
|
+
"""Adds two integers and returns their sum."""
|
|
25
|
+
return a + b
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(_):
|
|
29
|
+
mcp.run(transport='streamable-http', mount_path='/mcp')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
absl_app.run(main)
|
langfun/core/mcp/tool.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Copyright 2025 The Langfun Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""MCP tool."""
|
|
15
|
+
|
|
16
|
+
from typing import Annotated, Any, ClassVar
|
|
17
|
+
|
|
18
|
+
from langfun.core.structured import schema as lf_schema
|
|
19
|
+
import mcp
|
|
20
|
+
import pyglove as pg
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _McpToolMeta(pg.symbolic.ObjectMeta):
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return f'<tool-class \'{self.__name__}\'>'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class McpTool(pg.Object, metaclass=_McpToolMeta):
|
|
30
|
+
"""Base class for MCP tools."""
|
|
31
|
+
|
|
32
|
+
TOOL_NAME: Annotated[
|
|
33
|
+
ClassVar[str],
|
|
34
|
+
'Tool name.'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def python_definition(cls, markdown: bool = True) -> str:
|
|
39
|
+
"""Returns the Python definition of the tool."""
|
|
40
|
+
return lf_schema.Schema.from_value(cls).schema_str(
|
|
41
|
+
protocol='python', markdown=markdown
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __call__(
|
|
45
|
+
self,
|
|
46
|
+
session,
|
|
47
|
+
*,
|
|
48
|
+
returns_call_result: bool = False) -> Any:
|
|
49
|
+
"""Calls a MCP tool synchronously.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
session: A MCP session.
|
|
53
|
+
returns_call_result: If True, returns the call result. Otherwise returns
|
|
54
|
+
the result from structured content, or return content.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The call result, or the result from structured content, or content.
|
|
58
|
+
"""
|
|
59
|
+
return session.call_tool(
|
|
60
|
+
self,
|
|
61
|
+
returns_call_result=returns_call_result
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def acall(
|
|
65
|
+
self,
|
|
66
|
+
session,
|
|
67
|
+
*,
|
|
68
|
+
returns_call_result: bool = False) -> Any:
|
|
69
|
+
"""Calls a MCP tool asynchronously."""
|
|
70
|
+
return await session.acall_tool(self, returns_call_result=returns_call_result)
|
|
71
|
+
|
|
72
|
+
def input_parameters(self) -> dict[str, Any]:
|
|
73
|
+
"""Returns the input parameters of the tool."""
|
|
74
|
+
json = self.to_json()
|
|
75
|
+
def _transform(path: pg.KeyPath, x: Any) -> Any:
|
|
76
|
+
del path
|
|
77
|
+
if isinstance(x, dict):
|
|
78
|
+
x.pop(pg.JSONConvertible.TYPE_NAME_KEY, None)
|
|
79
|
+
return x
|
|
80
|
+
return pg.utils.transform(json, _transform)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def make_class(cls, tool_definition: mcp.Tool) -> type['McpTool']:
|
|
84
|
+
"""Makes a MCP tool class from tool definition."""
|
|
85
|
+
|
|
86
|
+
class _McpTool(cls):
|
|
87
|
+
auto_schema = False
|
|
88
|
+
|
|
89
|
+
tool_cls = _McpTool
|
|
90
|
+
tool_cls.TOOL_NAME = tool_definition.name
|
|
91
|
+
tool_cls.__name__ = _snake_to_camel(tool_definition.name)
|
|
92
|
+
tool_cls.__doc__ = tool_definition.description
|
|
93
|
+
schema = pg.Schema.from_json_schema(
|
|
94
|
+
tool_definition.inputSchema, class_fn=McpToolInput.make_class
|
|
95
|
+
)
|
|
96
|
+
tool_cls.apply_schema(schema)
|
|
97
|
+
return tool_cls
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _McpToolInputMeta(pg.symbolic.ObjectMeta):
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
return f'<input-class \'{self.__name__}\'>'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class McpToolInput(pg.Object, metaclass=_McpToolInputMeta):
|
|
107
|
+
"""Base class for MCP tool inputs."""
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def make_class(cls, name: str, schema: pg.Schema):
|
|
111
|
+
"""Converts a schema to an input class."""
|
|
112
|
+
|
|
113
|
+
class _McpToolInput(cls):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
input_cls = _McpToolInput
|
|
117
|
+
input_cls.__name__ = _snake_to_camel(name)
|
|
118
|
+
input_cls.__doc__ = schema.description
|
|
119
|
+
input_cls.apply_schema(schema)
|
|
120
|
+
return input_cls
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _snake_to_camel(name: str) -> str:
|
|
124
|
+
"""Converts a snake_case name to a CamelCase name."""
|
|
125
|
+
return ''.join(x.capitalize() for x in name.split('_'))
|
{langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langfun
|
|
3
|
-
Version: 0.1.2.
|
|
3
|
+
Version: 0.1.2.dev202510190804
|
|
4
4
|
Summary: Langfun: Language as Functions.
|
|
5
5
|
Home-page: https://github.com/google/langfun
|
|
6
6
|
Author: Langfun Authors
|
|
@@ -23,14 +23,16 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: anyio>=4.7.0
|
|
25
25
|
Requires-Dist: jinja2>=3.1.2
|
|
26
|
+
Requires-Dist: mcp>=1.17.0
|
|
26
27
|
Requires-Dist: puremagic>=1.20
|
|
27
|
-
Requires-Dist: pyglove>=0.
|
|
28
|
+
Requires-Dist: pyglove>=0.5.0.dev202510170226
|
|
28
29
|
Requires-Dist: requests>=2.31.0
|
|
29
30
|
Provides-Extra: all
|
|
30
31
|
Requires-Dist: anyio>=4.7.0; extra == "all"
|
|
31
32
|
Requires-Dist: jinja2>=3.1.2; extra == "all"
|
|
33
|
+
Requires-Dist: mcp>=1.17.0; extra == "all"
|
|
32
34
|
Requires-Dist: puremagic>=1.20; extra == "all"
|
|
33
|
-
Requires-Dist: pyglove>=0.
|
|
35
|
+
Requires-Dist: pyglove>=0.5.0.dev202510170226; extra == "all"
|
|
34
36
|
Requires-Dist: requests>=2.31.0; extra == "all"
|
|
35
37
|
Requires-Dist: google-auth>=2.16.0; extra == "all"
|
|
36
38
|
Requires-Dist: pillow>=10.0.0; extra == "all"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langfun/__init__.py,sha256=
|
|
1
|
+
langfun/__init__.py,sha256=Kx-9_ODBpqqXb3cY5oSS-Sr-ZfuyM59W511ca9VYdRY,2663
|
|
2
2
|
langfun/assistant/capabilities/gui/__init__.py,sha256=7-fWINsczHkEAT1hS4vVHnTW3JC_4PZW0E4HfjClOD8,1290
|
|
3
3
|
langfun/assistant/capabilities/gui/bounding_box_parser.py,sha256=wJSEJvt2WFH0o9C3z9uW_vK0qRAJWbkgX5gBAJU7w6k,5832
|
|
4
4
|
langfun/assistant/capabilities/gui/bounding_box_parser_test.py,sha256=9yBvt7fXT_BFVcOXUSm4K0mLdKRYwk2Y9zaCpYYgBnQ,10934
|
|
@@ -121,6 +121,13 @@ langfun/core/llms/cache/__init__.py,sha256=QAo3InUMDM_YpteNnVCSejI4zOsnjSMWKJKzk
|
|
|
121
121
|
langfun/core/llms/cache/base.py,sha256=rt3zwmyw0y9jsSGW-ZbV1vAfLxQ7_3AVk0l2EySlse4,3918
|
|
122
122
|
langfun/core/llms/cache/in_memory.py,sha256=i58oiQL28RDsq37dwqgVpC2mBETJjIEFS20yHiV5MKU,5185
|
|
123
123
|
langfun/core/llms/cache/in_memory_test.py,sha256=V2UPeu5co5vUwSkjekCH1B1iLm9qQKPaacvP6VW3GTg,10388
|
|
124
|
+
langfun/core/mcp/__init__.py,sha256=cyVP_YTjOmbjhYg8BE7-RnE4Txt8wDukYC0anhHKpuo,286
|
|
125
|
+
langfun/core/mcp/client.py,sha256=NBo4W2fdvYyywlUHynAbyh7LNohILccTkCiisj-UEEg,3642
|
|
126
|
+
langfun/core/mcp/client_test.py,sha256=Vjbh7bXM8xXMMe_QOnFJ4rE0wITovAZbwk7Nu-cqf8g,2766
|
|
127
|
+
langfun/core/mcp/session.py,sha256=dyuNXpLvM0TgLoUmWEwy5XCRf-IK7H_BbcNI2oFa_ek,5626
|
|
128
|
+
langfun/core/mcp/tool.py,sha256=eap46OK4lrw5G5uCwPi4kUMNdaN1Bgy0cUi0bdLJI5Q,3607
|
|
129
|
+
langfun/core/mcp/testing/simple_mcp_client.py,sha256=U5pUXa0SnB2-DdRI2vPhrdVUv14dX_OeS4mPLFpllMc,924
|
|
130
|
+
langfun/core/mcp/testing/simple_mcp_server.py,sha256=dw4t6ERWMdmi6kDE38RU5oYu5MQbEX-GJ6CMxGcV-WE,994
|
|
124
131
|
langfun/core/memories/__init__.py,sha256=HpghfZ-w1NQqzJXBx8Lz0daRhB2rcy2r9Xm491SBhC4,773
|
|
125
132
|
langfun/core/memories/conversation_history.py,sha256=KR78PurXTSeqsRK9QG9xM7-f245rs20EvWO7Mm2n2Vw,1827
|
|
126
133
|
langfun/core/memories/conversation_history_test.py,sha256=2kzAq2pUrbR01Z9jhxviIao52JK4JVjr5iGP8pwGxlU,2156
|
|
@@ -181,8 +188,8 @@ langfun/env/event_handlers/event_logger.py,sha256=3dbPjBe53dBgntYHlyLlj_77hVecPS
|
|
|
181
188
|
langfun/env/event_handlers/event_logger_test.py,sha256=PGof3rPllNnyzs3Yp8kaOHLeTkVrzUgCJwlODTrVRKI,9111
|
|
182
189
|
langfun/env/event_handlers/metric_writer.py,sha256=NgJKsd6xWOtEd0IjYi7coGEaqGYkkPcDjXN9CQ3vxPU,18043
|
|
183
190
|
langfun/env/event_handlers/metric_writer_test.py,sha256=flRqK10wonhJk4idGD_8jjEjrfjgH0R-qcu-7Bj1G5s,5335
|
|
184
|
-
langfun-0.1.2.
|
|
185
|
-
langfun-0.1.2.
|
|
186
|
-
langfun-0.1.2.
|
|
187
|
-
langfun-0.1.2.
|
|
188
|
-
langfun-0.1.2.
|
|
191
|
+
langfun-0.1.2.dev202510190804.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
192
|
+
langfun-0.1.2.dev202510190804.dist-info/METADATA,sha256=b4yNwslro5xun1m0Jz5h3E7rtFAEZmUrZ6WFK1E93LY,7522
|
|
193
|
+
langfun-0.1.2.dev202510190804.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
194
|
+
langfun-0.1.2.dev202510190804.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
|
|
195
|
+
langfun-0.1.2.dev202510190804.dist-info/RECORD,,
|
|
File without changes
|
{langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{langfun-0.1.2.dev202510170805.dist-info → langfun-0.1.2.dev202510190804.dist-info}/top_level.txt
RENAMED
|
File without changes
|