Skip to content

Commit 7d60f21

Browse files
authored
Merge branch 'main' into main
2 parents 010c5f7 + 40acbc5 commit 7d60f21

File tree

9 files changed

+229
-1138
lines changed

9 files changed

+229
-1138
lines changed

examples/clients/simple-auth-client/pyproject.toml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ classifiers = [
1414
"Programming Language :: Python :: 3",
1515
"Programming Language :: Python :: 3.10",
1616
]
17-
dependencies = [
18-
"click>=8.2.0",
19-
"mcp>=1.0.0",
20-
]
17+
dependencies = ["click>=8.2.0", "mcp"]
2118

2219
[project.scripts]
2320
mcp-simple-auth-client = "mcp_simple_auth_client.main:cli"
@@ -44,9 +41,3 @@ target-version = "py310"
4441

4542
[tool.uv]
4643
dev-dependencies = ["pyright>=1.1.379", "pytest>=8.3.3", "ruff>=0.6.9"]
47-
48-
[tool.uv.sources]
49-
mcp = { path = "../../../" }
50-
51-
[[tool.uv.index]]
52-
url = "https://pypi.org/simple"

examples/clients/simple-auth-client/uv.lock

Lines changed: 0 additions & 535 deletions
This file was deleted.

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ async def start(self) -> None:
401401
await self.cleanup_servers()
402402

403403

404-
async def main() -> None:
404+
async def run() -> None:
405405
"""Initialize and run the chat session."""
406406
config = Configuration()
407407
server_config = config.load_config("servers_config.json")
@@ -411,5 +411,9 @@ async def main() -> None:
411411
await chat_session.start()
412412

413413

414+
def main() -> None:
415+
asyncio.run(run())
416+
417+
414418
if __name__ == "__main__":
415-
asyncio.run(main())
419+
main()

examples/clients/simple-chatbot/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ classifiers = [
1717
dependencies = [
1818
"python-dotenv>=1.0.0",
1919
"requests>=2.31.0",
20-
"mcp>=1.0.0",
21-
"uvicorn>=0.32.1"
20+
"mcp",
21+
"uvicorn>=0.32.1",
2222
]
2323

2424
[project.scripts]
25-
mcp-simple-chatbot = "mcp_simple_chatbot.client:main"
25+
mcp-simple-chatbot = "mcp_simple_chatbot.main:main"
2626

2727
[build-system]
2828
requires = ["hatchling"]

examples/clients/simple-chatbot/uv.lock

Lines changed: 0 additions & 555 deletions
This file was deleted.

pyproject.toml

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ venv = ".venv"
9898
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
9999
# and refactor code that is not public.
100100
executionEnvironments = [
101-
{ root = "tests", reportUnusedFunction = false, reportPrivateUsage = false },
102-
{ root = "examples/servers", reportUnusedFunction = false },
101+
{ root = "tests", reportUnusedFunction = false, reportPrivateUsage = false },
102+
{ root = "examples/servers", reportUnusedFunction = false },
103103
]
104104

105105
[tool.ruff]
@@ -109,17 +109,17 @@ extend-exclude = ["README.md"]
109109

110110
[tool.ruff.lint]
111111
select = [
112-
"C4", # flake8-comprehensions
113-
"C90", # mccabe
114-
"E", # pycodestyle
115-
"F", # pyflakes
116-
"I", # isort
117-
"PERF", # Perflint
118-
"PL", # Pylint
119-
"UP", # pyupgrade
112+
"C4", # flake8-comprehensions
113+
"C90", # mccabe
114+
"E", # pycodestyle
115+
"F", # pyflakes
116+
"I", # isort
117+
"PERF", # Perflint
118+
"PL", # Pylint
119+
"UP", # pyupgrade
120120
]
121121
ignore = ["PERF203", "PLC0415", "PLR0402"]
122-
mccabe.max-complexity = 24 # Default is 10
122+
mccabe.max-complexity = 24 # Default is 10
123123

124124
[tool.ruff.lint.per-file-ignores]
125125
"__init__.py" = ["F401"]
@@ -128,13 +128,13 @@ mccabe.max-complexity = 24 # Default is 10
128128

129129
[tool.ruff.lint.pylint]
130130
allow-magic-value-types = ["bytes", "float", "int", "str"]
131-
max-args = 23 # Default is 5
132-
max-branches = 23 # Default is 12
133-
max-returns = 13 # Default is 6
134-
max-statements = 102 # Default is 50
131+
max-args = 23 # Default is 5
132+
max-branches = 23 # Default is 12
133+
max-returns = 13 # Default is 6
134+
max-statements = 102 # Default is 50
135135

136136
[tool.uv.workspace]
137-
members = ["examples/servers/*", "examples/snippets"]
137+
members = ["examples/clients/*", "examples/servers/*", "examples/snippets"]
138138

139139
[tool.uv.sources]
140140
mcp = { workspace = true }
@@ -154,16 +154,16 @@ filterwarnings = [
154154
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
155155
"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel",
156156
# pywin32 internal deprecation warning
157-
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning"
157+
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
158158
]
159159

160160
[tool.markdown.lint]
161-
default=true
162-
MD004=false # ul-style - Unordered list style
163-
MD007.indent=2 # ul-indent - Unordered list indentation
164-
MD013=false # line-length - Line length
165-
MD029=false # ol-prefix - Ordered list item prefix
166-
MD033=false # no-inline-html Inline HTML
167-
MD041=false # first-line-heading/first-line-h1
168-
MD046=false # indented-code-blocks
169-
MD059=false # descriptive-link-text
161+
default = true
162+
MD004 = false # ul-style - Unordered list style
163+
MD007.indent = 2 # ul-indent - Unordered list indentation
164+
MD013 = false # line-length - Line length
165+
MD029 = false # ol-prefix - Ordered list item prefix
166+
MD033 = false # no-inline-html Inline HTML
167+
MD041 = false # first-line-heading/first-line-h1
168+
MD046 = false # indented-code-blocks
169+
MD059 = false # descriptive-link-text

src/mcp/client/session.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,16 +328,19 @@ async def call_tool(
328328
arguments: dict[str, Any] | None = None,
329329
read_timeout_seconds: timedelta | None = None,
330330
progress_callback: ProgressFnT | None = None,
331+
*,
332+
meta: dict[str, Any] | None = None,
331333
) -> types.CallToolResult:
332334
"""Send a tools/call request with optional progress callback support."""
333335

336+
_meta: types.RequestParams.Meta | None = None
337+
if meta is not None:
338+
_meta = types.RequestParams.Meta(**meta)
339+
334340
result = await self.send_request(
335341
types.ClientRequest(
336342
types.CallToolRequest(
337-
params=types.CallToolRequestParams(
338-
name=name,
339-
arguments=arguments,
340-
),
343+
params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=_meta),
341344
)
342345
),
343346
types.CallToolResult,

tests/client/test_session.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
1212
from mcp.types import (
1313
LATEST_PROTOCOL_VERSION,
14+
CallToolResult,
1415
ClientNotification,
1516
ClientRequest,
1617
Implementation,
@@ -23,6 +24,7 @@
2324
JSONRPCResponse,
2425
ServerCapabilities,
2526
ServerResult,
27+
TextContent,
2628
)
2729

2830

@@ -492,8 +494,125 @@ async def mock_server():
492494

493495
# Assert that capabilities are properly set with custom callbacks
494496
assert received_capabilities is not None
495-
assert received_capabilities.sampling is not None # Custom sampling callback provided
497+
# Custom sampling callback provided
498+
assert received_capabilities.sampling is not None
496499
assert isinstance(received_capabilities.sampling, types.SamplingCapability)
497-
assert received_capabilities.roots is not None # Custom list_roots callback provided
500+
# Custom list_roots callback provided
501+
assert received_capabilities.roots is not None
498502
assert isinstance(received_capabilities.roots, types.RootsCapability)
499-
assert received_capabilities.roots.listChanged is True # Should be True for custom callback
503+
# Should be True for custom callback
504+
assert received_capabilities.roots.listChanged is True
505+
506+
507+
@pytest.mark.anyio
508+
@pytest.mark.parametrize(argnames="meta", argvalues=[None, {"toolMeta": "value"}])
509+
async def test_client_tool_call_with_meta(meta: dict[str, Any] | None):
510+
"""Test that client tool call requests can include metadata"""
511+
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
512+
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
513+
514+
mocked_tool = types.Tool(name="sample_tool", inputSchema={})
515+
516+
async def mock_server():
517+
# Receive initialization request from client
518+
session_message = await client_to_server_receive.receive()
519+
jsonrpc_request = session_message.message
520+
assert isinstance(jsonrpc_request.root, JSONRPCRequest)
521+
request = ClientRequest.model_validate(
522+
jsonrpc_request.model_dump(by_alias=True, mode="json", exclude_none=True)
523+
)
524+
assert isinstance(request.root, InitializeRequest)
525+
526+
result = ServerResult(
527+
InitializeResult(
528+
protocolVersion=LATEST_PROTOCOL_VERSION,
529+
capabilities=ServerCapabilities(),
530+
serverInfo=Implementation(name="mock-server", version="0.1.0"),
531+
)
532+
)
533+
534+
# Answer initialization request
535+
await server_to_client_send.send(
536+
SessionMessage(
537+
JSONRPCMessage(
538+
JSONRPCResponse(
539+
jsonrpc="2.0",
540+
id=jsonrpc_request.root.id,
541+
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
542+
)
543+
)
544+
)
545+
)
546+
547+
# Receive initialized notification
548+
await client_to_server_receive.receive()
549+
550+
# Wait for the client to send a 'tools/call' request
551+
session_message = await client_to_server_receive.receive()
552+
jsonrpc_request = session_message.message
553+
assert isinstance(jsonrpc_request.root, JSONRPCRequest)
554+
555+
assert jsonrpc_request.root.method == "tools/call"
556+
557+
if meta is not None:
558+
assert jsonrpc_request.root.params
559+
assert "_meta" in jsonrpc_request.root.params
560+
assert jsonrpc_request.root.params["_meta"] == meta
561+
562+
result = ServerResult(
563+
CallToolResult(content=[TextContent(type="text", text="Called successfully")], isError=False)
564+
)
565+
566+
# Send the tools/call result
567+
await server_to_client_send.send(
568+
SessionMessage(
569+
JSONRPCMessage(
570+
JSONRPCResponse(
571+
jsonrpc="2.0",
572+
id=jsonrpc_request.root.id,
573+
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
574+
)
575+
)
576+
)
577+
)
578+
579+
# Wait for the tools/list request from the client
580+
# The client requires this step to validate the tool output schema
581+
session_message = await client_to_server_receive.receive()
582+
jsonrpc_request = session_message.message
583+
assert isinstance(jsonrpc_request.root, JSONRPCRequest)
584+
585+
assert jsonrpc_request.root.method == "tools/list"
586+
587+
result = types.ListToolsResult(tools=[mocked_tool])
588+
589+
await server_to_client_send.send(
590+
SessionMessage(
591+
JSONRPCMessage(
592+
JSONRPCResponse(
593+
jsonrpc="2.0",
594+
id=jsonrpc_request.root.id,
595+
result=result.model_dump(by_alias=True, mode="json", exclude_none=True),
596+
)
597+
)
598+
)
599+
)
600+
601+
server_to_client_send.close()
602+
603+
async with (
604+
ClientSession(
605+
server_to_client_receive,
606+
client_to_server_send,
607+
) as session,
608+
anyio.create_task_group() as tg,
609+
client_to_server_send,
610+
client_to_server_receive,
611+
server_to_client_send,
612+
server_to_client_receive,
613+
):
614+
tg.start_soon(mock_server)
615+
616+
await session.initialize()
617+
618+
await session.call_tool(name=mocked_tool.name, arguments={"foo": "bar"}, meta=meta)

uv.lock

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)