Skip to content

Instantly share code, notes, and snippets.

@pgtwitter
Last active June 27, 2025 16:24
Show Gist options
  • Save pgtwitter/c97ad1088ed08623b5f5d6b9cf0013cc to your computer and use it in GitHub Desktop.
Save pgtwitter/c97ad1088ed08623b5f5d6b9cf0013cc to your computer and use it in GitHub Desktop.
Ollamaで動かしたqwen3:14b で MCP クライアント / サーバ の実験 (プロンプト: ~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.)

ハード

$ system_profiler SPHardwareDataType|grep "Chip\|Memory"
      Chip: Apple M1
      Memory: 16 GB

brewでollamaをインストール

$ brew install ollama

インストールした時に表示されたままに環境変数を設定しておく

$ env|grep OLLAMA
OLLAMA_KV_CACHE_TYPE=q8_0
OLLAMA_FLASH_ATTENTION=1

ollama を起動する

$ ollama serve
…

qwen3:14b があるか確認

$ ollama list
NAME                ID              SIZE      MODIFIED     
qwen3:14b           ------------    9.3 GB    - hours ago     

なければ pull

$ ollama pull qwen3:14b   

MCP サーバの起動

$ python mcp_server.py
2025-06-28 00:31:58,029 - DEBUG - Using selector: KqueueSelector
2025-06-28 00:31:58,063 - INFO - server listening on 127.0.0.1:8080
2025-06-28 00:31:58,063 - INFO - server listening on [::1]:8080
MCP server running at ws://localhost:8080
…

MCP クライアントの起動

$ python mcp_client.py
~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.
…

ollama の実行状況を確認

$ ollama ps
NAME         ID              SIZE     PROCESSOR    UNTIL              
qwen3:14b    ------------    10 GB    100% GPU     - minutes from now  

MCP サーバ の停止

Ctrl+C

ollama の停止

Ctrl+C

2025-06-28 00:58:22,698 - INFO - Processing prompt: ~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.
2025-06-28 00:58:22,776 - INFO - Starting discover_tools
2025-06-28 00:58:22,812 - INFO - Sending initialize request
2025-06-28 00:58:22,815 - INFO - Received initialize response: {'id': 1, 'result': {'capabilities': {}}}
2025-06-28 00:58:22,815 - INFO - Sending discover_tools request
2025-06-28 00:58:22,817 - INFO - Received discover_tools response: {'id': 2, 'result': [{'name': 'search_files', 'description': "Searches files in ~/Public using a single file name pattern (e.g., '*.txt', '*.pdf'). Call separately for multiple patterns. Invalid patterns (e.g., containing '/' or starting with '..') return an empty list.", 'parameters': {'type': 'object', 'properties': {'pattern': {'type': 'string', 'description': "A single file name pattern (e.g., '*.txt', '*.pdf'). Must not include path components (e.g., '~/Public/*.txt') or '/' nor start with '..' (e.g., '../*.txt'). Call separately for each pattern."}}, 'required': ['pattern']}}, {'name': 'add_numbers', 'description': 'Adds two numbers and returns their sum (e.g., 3 + 5).', 'parameters': {'type': 'object', 'properties': {'a': {'type': 'number', 'description': 'The first number (integer or float).'}, 'b': {'type': 'number', 'description': 'The second number (integer or float).'}}, 'required': ['a', 'b']}}]}
2025-06-28 00:58:22,822 - INFO - Discovered tools: [{'type': 'function', 'function': {'name': 'search_files', 'description': "Searches files in ~/Public using a single file name pattern (e.g., '*.txt', '*.pdf'). Call separately for multiple patterns. Invalid patterns (e.g., containing '/' or starting with '..') return an empty list.", 'parameters': {'type': 'object', 'properties': {'pattern': {'type': 'string', 'description': "A single file name pattern (e.g., '*.txt', '*.pdf'). Must not include path components (e.g., '~/Public/*.txt') or '/' nor start with '..' (e.g., '../*.txt'). Call separately for each pattern."}}, 'required': ['pattern']}}}, {'type': 'function', 'function': {'name': 'add_numbers', 'description': 'Adds two numbers and returns their sum (e.g., 3 + 5).', 'parameters': {'type': 'object', 'properties': {'a': {'type': 'number', 'description': 'The first number (integer or float).'}, 'b': {'type': 'number', 'description': 'The second number (integer or float).'}}, 'required': ['a', 'b']}}}]
2025-06-28 00:58:22,822 - INFO - Sending LLM request with messages: [{'role': 'user', 'content': '~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.'}]
2025-06-28 00:59:05,527 - INFO - HTTP Request: POST http://localhost:11434/v1/chat/completions "HTTP/1.1 200 OK"
2025-06-28 01:01:57,451 - INFO - Response content: <think>
Okay, let's see. The user is asking about the number of .txt and .world files in the ~/Public folder. They want the total count, and they specified that addition must use a tool.
First, I need to check the available functions. There's search_files which can take a pattern. The pattern should be like '*.txt' or '*.pdf'. The user mentioned both .txt and .world, so I need to call search_files twice, once for each pattern.
Wait, the function's description says to call separately for multiple patterns. So first, I'll call search_files with '*.txt' to get the count of text files. Then, another call with '*.world' for the world files. Each call will return a list of files matching the pattern. The length of each list will be the count.
Once I have both counts, I need to add them. The add_numbers function takes two numbers, a and b. So after getting the two numbers from the search results, I'll use add_numbers to sum them. The result will be the total number of files.
But wait, the user is in Japanese, but the function parameters are in English. The patterns are '*.txt' and '*.world'. I need to make sure the function calls are correctly formatted. Also, the function returns an empty list for invalid patterns, but the user's patterns are valid, so it should work.
So the steps are: call search_files for .txt, get count1; call search_files for .world, get count2; then add count1 and count2 using add_numbers. Then present the total.
</think>
2025-06-28 01:01:57,466 - INFO - Response tool_calls: [ChoiceDeltaToolCall(index=0, id='call_etz0xups', function=ChoiceDeltaToolCallFunction(arguments='{"pattern":"*.txt"}', name='search_files'), type='function'), ChoiceDeltaToolCall(index=1, id='call_uyg130c8', function=ChoiceDeltaToolCallFunction(arguments='{"pattern":"*.world"}', name='search_files'), type='function')]
2025-06-28 01:01:57,466 - INFO - Calling tool: search_files with args: {"pattern":"*.txt"}
2025-06-28 01:01:57,724 - INFO - Initialized WebSocket connection
2025-06-28 01:01:57,737 - INFO - Received tool response: {'id': 'call_etz0xups', 'result': {'files': ['HelloWorld.txt', 'FooBar.txt']}}
2025-06-28 01:01:57,742 - INFO - Appended tool result to messages: {'files': ['HelloWorld.txt', 'FooBar.txt']}
2025-06-28 01:01:57,743 - INFO - Sending LLM request with messages: [{'role': 'user', 'content': '~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.'}, {'role': 'tool', 'content': '{"files": ["HelloWorld.txt", "FooBar.txt"]}', 'tool_call_id': 'call_etz0xups'}]
2025-06-28 01:02:01,999 - INFO - HTTP Request: POST http://localhost:11434/v1/chat/completions "HTTP/1.1 200 OK"
2025-06-28 01:06:19,899 - INFO - Response content: <think>
Okay, let's see. The user is asking about the number of .txt and .world files in the ~/Public folder. They want the total count, and they specified that addition must use a tool.
First, I need to check the available functions. There's search_files for searching files with a pattern and add_numbers for adding two numbers.
The user's question has two parts: counting .txt files and .world files. The search_files function can be used for each pattern. But the user mentioned that addition should be done via the tool. So, I need to first get the counts of each file type using search_files, then add them with add_numbers.
Wait, the initial response from the tool for .txt files returned 2 files. But the user also asked about .world files. I need to call search_files again with the pattern *.world to get that count. Then, add both numbers using add_numbers.
But in the given example, the assistant only called search_files once for .txt. The user's question includes both .txt and .world, so I need to make sure both are counted. Maybe the initial response was just for .txt, but the user's question requires both. So, the next step is to call search_files for .world files. Then, add the two counts using add_numbers.
Wait, the user's question is in Japanese, but the function parameters require the pattern in English. So, the patterns would be "*.txt" and "*.world". The user might have a typo, like "world" instead of "world" (but probably not). Assuming the user meant .world files.
So, first, call search_files with "*.txt" to get the count (which was 2 in the example). Then call search_files with "*.world" to get the number of .world files. Then, add those two numbers using add_numbers.
But in the given example, the assistant only called search_files once. Maybe the user's question was split into two parts, but the initial response was only for .txt. However, the user's question asks for both .txt and .world files. Therefore, the assistant needs to make two separate calls to search_files for each pattern and then add the results.
So, the correct approach is: first, call search_files with "*.txt", then call it again with "*.world", get both counts, then use add_numbers to sum them. The initial response only did the first part, but the user's question requires both. Therefore, the assistant needs to proceed with the second search and then the addition.
</think>
2025-06-28 01:06:19,912 - INFO - Response tool_calls: [ChoiceDeltaToolCall(index=0, id='call_siy41z4d', function=ChoiceDeltaToolCallFunction(arguments='{"pattern":"*.world"}', name='search_files'), type='function')]
2025-06-28 01:06:19,914 - INFO - Calling tool: search_files with args: {"pattern":"*.world"}
2025-06-28 01:06:20,014 - INFO - Initialized WebSocket connection
2025-06-28 01:06:20,025 - INFO - Received tool response: {'id': 'call_siy41z4d', 'result': {'files': ['hello.world']}}
2025-06-28 01:06:20,037 - INFO - Appended tool result to messages: {'files': ['hello.world']}
2025-06-28 01:06:20,037 - INFO - Sending LLM request with messages: [{'role': 'user', 'content': '~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.'}, {'role': 'tool', 'content': '{"files": ["HelloWorld.txt", "FooBar.txt"]}', 'tool_call_id': 'call_etz0xups'}, {'role': 'tool', 'content': '{"files": ["hello.world"]}', 'tool_call_id': 'call_siy41z4d'}]
2025-06-28 01:06:22,131 - INFO - HTTP Request: POST http://localhost:11434/v1/chat/completions "HTTP/1.1 200 OK"
2025-06-28 01:08:07,993 - INFO - Response content: <think>
Okay, let's see. The user is asking about the number of .txt and .world files in the ~/Public folder. They want the total count, and they specified that addition must use a tool.
First, I need to check the tool functions available. There's search_files for finding files with a pattern and add_numbers for adding two numbers.
The user's question has two parts: counting .txt files and .world files. The search_files function can be used for each pattern. The first call would be for '*.txt' and the second for '*.world'.
Looking at the tool responses provided, the first call returned 2 .txt files and the second call returned 1 .world file. Now, I need to add these two numbers together. The add_numbers function is the right tool here.
So, I'll use add_numbers with a=2 and b=1. The result should be 3. Then, present the total count as the answer.
</think>
2025-06-28 01:08:07,996 - INFO - Response tool_calls: [ChoiceDeltaToolCall(index=0, id='call_lyzzmipu', function=ChoiceDeltaToolCallFunction(arguments='{"a":2,"b":1}', name='add_numbers'), type='function')]
2025-06-28 01:08:07,996 - INFO - Calling tool: add_numbers with args: {"a":2,"b":1}
2025-06-28 01:08:08,108 - INFO - Initialized WebSocket connection
2025-06-28 01:08:08,134 - INFO - Received tool response: {'id': 'call_lyzzmipu', 'result': {'sum': 3}}
2025-06-28 01:08:08,141 - INFO - Appended tool result to messages: {'sum': 3}
2025-06-28 01:08:08,145 - INFO - Sending LLM request with messages: [{'role': 'user', 'content': '~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください.'}, {'role': 'tool', 'content': '{"files": ["HelloWorld.txt", "FooBar.txt"]}', 'tool_call_id': 'call_etz0xups'}, {'role': 'tool', 'content': '{"files": ["hello.world"]}', 'tool_call_id': 'call_siy41z4d'}, {'role': 'tool', 'content': '{"sum": 3}', 'tool_call_id': 'call_lyzzmipu'}]
2025-06-28 01:08:10,351 - INFO - HTTP Request: POST http://localhost:11434/v1/chat/completions "HTTP/1.1 200 OK"
2025-06-28 01:10:27,749 - INFO - Response content: <think>
Okay, let's see. The user is asking about the number of .txt and .world files in the ~/Public folder. They also mentioned to use the tool for addition.
First, I need to check the tool responses. The first tool call for .txt files returned 2 files: "HelloWorld.txt" and "FooBar.txt". The second call for .world files found one file: "hello.world".
Wait, the user asked for .txt and .world files. The first tool response has two .txt files, the second has one .world file. So adding those together: 2 + 1 = 3.
The third tool call was add_numbers with 2 and 1, which correctly gave 3.
So the answer should state there are 2 .txt files and 1 .world file, totaling 3. Make sure to mention using the add_numbers tool for the sum.
</think>
~/Publicフォルダ下に存在する拡張子txtのファイルは2つ(HelloWorld.txt, FooBar.txt)、拡張子worldのファイルは1つ(hello.world)あります。
これらのファイル数の合計は、ツールを使用して計算すると **2 + 1 = 3** です。
2025-06-28 01:10:27,752 - INFO - Final result: <think>
Okay, let's see. The user is asking about the number of .txt and .world files in the ~/Public folder. They also mentioned to use the tool for addition.
First, I need to check the tool responses. The first tool call for .txt files returned 2 files: "HelloWorld.txt" and "FooBar.txt". The second call for .world files found one file: "hello.world".
Wait, the user asked for .txt and .world files. The first tool response has two .txt files, the second has one .world file. So adding those together: 2 + 1 = 3.
The third tool call was add_numbers with 2 and 1, which correctly gave 3.
So the answer should state there are 2 .txt files and 1 .world file, totaling 3. Make sure to mention using the add_numbers tool for the sum.
</think>
~/Publicフォルダ下に存在する拡張子txtのファイルは2つ(HelloWorld.txt, FooBar.txt)、拡張子worldのファイルは1つ(hello.world)あります。
これらのファイル数の合計は、ツールを使用して計算すると **2 + 1 = 3** です。
import asyncio
import json
import logging
import websockets
from openai import OpenAI
# ロギング設定
logging.basicConfig(
# level=logging.DEBUG,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('mcp_client.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
async def discover_tools(server_address="ws://localhost:8080"):
"""MCPサーバーからツールリストを取得"""
logger.info("Starting discover_tools")
try:
async with websockets.connect(server_address) as websocket:
# 初期化
logger.info("Sending initialize request")
await websocket.send(json.dumps({"id": 1, "method": "initialize", "params": {}}))
init_response = json.loads(await websocket.recv())
logger.info(f"Received initialize response: {init_response}")
if init_response.get("error"):
logger.error(f"Initialization failed: {init_response['error']}")
raise Exception(f"Initialization failed: {init_response['error']}")
# ツール発見
logger.info("Sending discover_tools request")
await websocket.send(json.dumps({"id": 2, "method": "discover_tools"}))
tools_response = json.loads(await websocket.recv())
logger.info(f"Received discover_tools response: {tools_response}")
if tools_response.get("error"):
logger.error(f"Tool discovery failed: {tools_response['error']}")
raise Exception(f"Tool discovery failed: {tools_response['error']}")
return tools_response.get("result", [])
except Exception as e:
logger.error(f"Error in discover_tools: {e}")
return []
async def call_tool(server_address, tool_call):
"""MCPサーバーにツール呼び出しを送信"""
logger.info(f"Calling tool: {tool_call.function.name} with args: {tool_call.function.arguments}")
try:
async with websockets.connect(server_address) as websocket:
# 初期化
await websocket.send(json.dumps({"id": 1, "method": "initialize", "params": {}}))
await websocket.recv()
logger.info("Initialized WebSocket connection")
# ツール呼び出し
await websocket.send(json.dumps({
"id": tool_call.id,
"method": tool_call.function.name,
"params": json.loads(tool_call.function.arguments)
}))
response = json.loads(await websocket.recv())
logger.info(f"Received tool response: {response}")
if response.get("error"):
logger.error(f"Tool call failed: {response['error']}")
raise Exception(f"Tool call failed: {response['error']}")
return response.get("result")
except Exception as e:
logger.error(f"Error in call_tool: {e}")
return {"error": str(e)}
async def handle_mcp_request(client, messages, tools, server_address="ws://localhost:8080"):
"""LLMとMCPサーバーを中継(ストリーミング対応)"""
logger.info(f"Sending LLM request with messages: {messages}")
try:
response = client.chat.completions.create(
model=client.model_name,
messages=messages,
tools=tools,
temperature=0.0,
stream=True # ストリーミング有効
)
content = ""
tool_calls = []
for chunk in response:
delta = chunk.choices[0].delta
if delta.content:
content += delta.content
logger.debug(f"Streaming content: {delta.content}")
if delta.tool_calls:
tool_calls.extend(delta.tool_calls)
logger.debug(f"Streaming tool_calls: {delta.tool_calls}")
if len(content) > 0:
logger.info(f"Response content: {content}")
if len(tool_calls) > 0:
logger.info(f"Response tool_calls: {tool_calls}")
if tool_calls:
tool_call = tool_calls[0]
result = await call_tool(server_address, tool_call)
if "error" in result:
messages.append({
"role": "tool",
"content": json.dumps(result),
"tool_call_id": tool_call.id
})
logger.info(f"Tool call error, retrying: {result}")
return await handle_mcp_request(client, messages, tools, server_address)
messages.append({
"role": "tool",
"content": json.dumps(result),
"tool_call_id": tool_call.id
})
logger.info(f"Appended tool result to messages: {result}")
return await handle_mcp_request(client, messages, tools, server_address)
return content
except Exception as e:
logger.error(f"Error in handle_mcp_request: {e}")
return f"LLM request failed: {e}"
async def process_prompt(prompt, model_name="qwen3:14b"):
"""ユーザーのプロンプトを処理"""
logger.info(f"Processing prompt: {prompt}")
try:
client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed")
client.model_name = model_name
# ツール取得
tools = await discover_tools()
if not tools:
logger.error("Failed to discover tools")
return "Failed to discover tools from MCP server."
tools = [{"type": "function", "function": tool} for tool in tools]
logger.info(f"Discovered tools: {tools}")
# システムプロンプト
messages = [
# {
# "role": "system",
# "content": (
# "Follow these steps using MCP JSON-RPC:\n"
# "1. Call search_files with {'pattern': '*.txt'}.\n"
# "2. Call search_files with {'pattern': '*.world'}.\n"
# "3. Call add_numbers with counts from steps 1 and 2.\n"
# "Generate ONE tool call per response."
# )
# },
{"role": "user", "content": prompt}
]
# MCPリクエスト処理
result = await handle_mcp_request(client, messages, tools)
logger.info(f"Final result: {result}")
return result
except Exception as e:
logger.error(f"Error in process_prompt: {e}")
return f"Process failed: {e}"
if __name__ == "__main__":
prompt = "~/Publicフォルダ下にある,拡張子txtのファイルと拡張子worldのファイルは合わせていくつありますか? 足し算は必ずツールを使ってください."
print(prompt)
model_name = "qwen3:14b"
result = asyncio.run(process_prompt(prompt, model_name=model_name))
print(result)
2025-06-28 00:58:18,111 - DEBUG - Using selector: KqueueSelector
2025-06-28 00:58:18,140 - INFO - server listening on 127.0.0.1:8080
2025-06-28 00:58:18,141 - INFO - server listening on [::1]:8080
2025-06-28 00:58:22,806 - DEBUG - = connection is CONNECTING
2025-06-28 00:58:22,807 - DEBUG - < GET / HTTP/1.1
2025-06-28 00:58:22,808 - DEBUG - < Host: localhost:8080
2025-06-28 00:58:22,808 - DEBUG - < Upgrade: websocket
2025-06-28 00:58:22,808 - DEBUG - < Connection: Upgrade
2025-06-28 00:58:22,808 - DEBUG - < Sec-WebSocket-Key: MNvter5VKijcq/ZWNKp5yw==
2025-06-28 00:58:22,808 - DEBUG - < Sec-WebSocket-Version: 13
2025-06-28 00:58:22,808 - DEBUG - < Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
2025-06-28 00:58:22,808 - DEBUG - < User-Agent: Python/3.10 websockets/10.4
2025-06-28 00:58:22,809 - DEBUG - > HTTP/1.1 101 Switching Protocols
2025-06-28 00:58:22,810 - DEBUG - > Upgrade: websocket
2025-06-28 00:58:22,810 - DEBUG - > Connection: Upgrade
2025-06-28 00:58:22,810 - DEBUG - > Sec-WebSocket-Accept: rXrXzvkmn63Jp0J1qwpBQYEpCi0=
2025-06-28 00:58:22,810 - DEBUG - > Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=12; client_max_window_bits=12
2025-06-28 00:58:22,810 - DEBUG - > Date: Fri, 27 Jun 2025 15:58:22 GMT
2025-06-28 00:58:22,810 - DEBUG - > Server: Python/3.10 websockets/10.4
2025-06-28 00:58:22,811 - INFO - connection open
2025-06-28 00:58:22,811 - DEBUG - = connection is OPEN
2025-06-28 00:58:22,811 - DEBUG - Initializing server 'SearchServer'
2025-06-28 00:58:22,813 - DEBUG - < TEXT '{"id": 1, "method": "initialize", "params": {}}' [47 bytes]
2025-06-28 00:58:22,814 - DEBUG - Received: {"id": 1, "method": "initialize", "params": {}}
2025-06-28 00:58:22,814 - DEBUG - Method: initialize
2025-06-28 00:58:22,814 - DEBUG - > TEXT '{"id": 1, "result": {"capabilities": {}}}' [41 bytes]
2025-06-28 00:58:22,816 - DEBUG - < TEXT '{"id": 2, "method": "discover_tools"}' [37 bytes]
2025-06-28 00:58:22,816 - DEBUG - Received: {"id": 2, "method": "discover_tools"}
2025-06-28 00:58:22,816 - DEBUG - Method: discover_tools
2025-06-28 00:58:22,816 - DEBUG - > TEXT '{"id": 2, "result": [{"name": "search_files", "...quired": ["a", "b"]}}]}' [922 bytes]
2025-06-28 00:58:22,819 - DEBUG - < CLOSE 1000 (OK) [2 bytes]
2025-06-28 00:58:22,819 - DEBUG - = connection is CLOSING
2025-06-28 00:58:22,820 - DEBUG - > CLOSE 1000 (OK) [2 bytes]
2025-06-28 00:58:22,820 - DEBUG - x half-closing TCP connection
2025-06-28 00:58:22,822 - DEBUG - = connection is CLOSED
2025-06-28 00:58:22,822 - INFO - connection closed
2025-06-28 01:01:57,666 - DEBUG - = connection is CONNECTING
2025-06-28 01:01:57,688 - DEBUG - < GET / HTTP/1.1
2025-06-28 01:01:57,689 - DEBUG - < Host: localhost:8080
2025-06-28 01:01:57,691 - DEBUG - < Upgrade: websocket
2025-06-28 01:01:57,691 - DEBUG - < Connection: Upgrade
2025-06-28 01:01:57,692 - DEBUG - < Sec-WebSocket-Key: 6shuDnVkV2mAE97ypw/FNQ==
2025-06-28 01:01:57,694 - DEBUG - < Sec-WebSocket-Version: 13
2025-06-28 01:01:57,696 - DEBUG - < Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
2025-06-28 01:01:57,697 - DEBUG - < User-Agent: Python/3.10 websockets/10.4
2025-06-28 01:01:57,709 - DEBUG - > HTTP/1.1 101 Switching Protocols
2025-06-28 01:01:57,709 - DEBUG - > Upgrade: websocket
2025-06-28 01:01:57,709 - DEBUG - > Connection: Upgrade
2025-06-28 01:01:57,709 - DEBUG - > Sec-WebSocket-Accept: rqU45dofQIECbX5PQ7sG0cG99/o=
2025-06-28 01:01:57,709 - DEBUG - > Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=12; client_max_window_bits=12
2025-06-28 01:01:57,709 - DEBUG - > Date: Fri, 27 Jun 2025 16:01:57 GMT
2025-06-28 01:01:57,709 - DEBUG - > Server: Python/3.10 websockets/10.4
2025-06-28 01:01:57,711 - INFO - connection open
2025-06-28 01:01:57,711 - DEBUG - = connection is OPEN
2025-06-28 01:01:57,718 - DEBUG - Initializing server 'SearchServer'
2025-06-28 01:01:57,722 - DEBUG - < TEXT '{"id": 1, "method": "initialize", "params": {}}' [47 bytes]
2025-06-28 01:01:57,722 - DEBUG - Received: {"id": 1, "method": "initialize", "params": {}}
2025-06-28 01:01:57,723 - DEBUG - Method: initialize
2025-06-28 01:01:57,723 - DEBUG - > TEXT '{"id": 1, "result": {"capabilities": {}}}' [41 bytes]
2025-06-28 01:01:57,725 - DEBUG - < TEXT '{"id": "call_etz0xups", "method": "search_files...: {"pattern": "*.txt"}}' [81 bytes]
2025-06-28 01:01:57,726 - DEBUG - Received: {"id": "call_etz0xups", "method": "search_files", "params": {"pattern": "*.txt"}}
2025-06-28 01:01:57,726 - DEBUG - Method: search_files
2025-06-28 01:01:57,726 - DEBUG - Calling tool: search_files, arguments: {'pattern': '*.txt'}
2025-06-28 01:01:57,729 - DEBUG - Search path: /Users/Work/Public/*.txt
2025-06-28 01:01:57,734 - DEBUG - > TEXT '{"id": "call_etz0xups", "result": {"files": ["H...d.txt", "FooBar.txt"]}}' [78 bytes]
2025-06-28 01:01:57,739 - DEBUG - < CLOSE 1000 (OK) [2 bytes]
2025-06-28 01:01:57,739 - DEBUG - = connection is CLOSING
2025-06-28 01:01:57,740 - DEBUG - > CLOSE 1000 (OK) [2 bytes]
2025-06-28 01:01:57,741 - DEBUG - x half-closing TCP connection
2025-06-28 01:01:57,742 - DEBUG - = connection is CLOSED
2025-06-28 01:01:57,744 - INFO - connection closed
2025-06-28 01:06:19,968 - DEBUG - = connection is CONNECTING
2025-06-28 01:06:19,990 - DEBUG - < GET / HTTP/1.1
2025-06-28 01:06:19,990 - DEBUG - < Host: localhost:8080
2025-06-28 01:06:19,991 - DEBUG - < Upgrade: websocket
2025-06-28 01:06:19,991 - DEBUG - < Connection: Upgrade
2025-06-28 01:06:19,991 - DEBUG - < Sec-WebSocket-Key: lxMAUc3XEuFZdaciQkb6JA==
2025-06-28 01:06:19,991 - DEBUG - < Sec-WebSocket-Version: 13
2025-06-28 01:06:19,991 - DEBUG - < Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
2025-06-28 01:06:19,991 - DEBUG - < User-Agent: Python/3.10 websockets/10.4
2025-06-28 01:06:19,998 - DEBUG - > HTTP/1.1 101 Switching Protocols
2025-06-28 01:06:19,998 - DEBUG - > Upgrade: websocket
2025-06-28 01:06:19,999 - DEBUG - > Connection: Upgrade
2025-06-28 01:06:19,999 - DEBUG - > Sec-WebSocket-Accept: Eck8y7hS2qKb+Llsj9gEX7eY/UU=
2025-06-28 01:06:19,999 - DEBUG - > Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=12; client_max_window_bits=12
2025-06-28 01:06:19,999 - DEBUG - > Date: Fri, 27 Jun 2025 16:06:19 GMT
2025-06-28 01:06:19,999 - DEBUG - > Server: Python/3.10 websockets/10.4
2025-06-28 01:06:19,999 - INFO - connection open
2025-06-28 01:06:19,999 - DEBUG - = connection is OPEN
2025-06-28 01:06:20,001 - DEBUG - Initializing server 'SearchServer'
2025-06-28 01:06:20,010 - DEBUG - < TEXT '{"id": 1, "method": "initialize", "params": {}}' [47 bytes]
2025-06-28 01:06:20,010 - DEBUG - Received: {"id": 1, "method": "initialize", "params": {}}
2025-06-28 01:06:20,011 - DEBUG - Method: initialize
2025-06-28 01:06:20,011 - DEBUG - > TEXT '{"id": 1, "result": {"capabilities": {}}}' [41 bytes]
2025-06-28 01:06:20,015 - DEBUG - < TEXT '{"id": "call_siy41z4d", "method": "search_files...{"pattern": "*.world"}}' [83 bytes]
2025-06-28 01:06:20,015 - DEBUG - Received: {"id": "call_siy41z4d", "method": "search_files", "params": {"pattern": "*.world"}}
2025-06-28 01:06:20,016 - DEBUG - Method: search_files
2025-06-28 01:06:20,016 - DEBUG - Calling tool: search_files, arguments: {'pattern': '*.world'}
2025-06-28 01:06:20,016 - DEBUG - Search path: /Users/Work/Public/*.world
2025-06-28 01:06:20,024 - DEBUG - > TEXT '{"id": "call_siy41z4d", "result": {"files": ["hello.world"]}}' [61 bytes]
2025-06-28 01:06:20,027 - DEBUG - < CLOSE 1000 (OK) [2 bytes]
2025-06-28 01:06:20,028 - DEBUG - = connection is CLOSING
2025-06-28 01:06:20,028 - DEBUG - > CLOSE 1000 (OK) [2 bytes]
2025-06-28 01:06:20,028 - DEBUG - x half-closing TCP connection
2025-06-28 01:06:20,037 - DEBUG - = connection is CLOSED
2025-06-28 01:06:20,038 - INFO - connection closed
2025-06-28 01:08:08,044 - DEBUG - = connection is CONNECTING
2025-06-28 01:08:08,074 - DEBUG - < GET / HTTP/1.1
2025-06-28 01:08:08,074 - DEBUG - < Host: localhost:8080
2025-06-28 01:08:08,074 - DEBUG - < Upgrade: websocket
2025-06-28 01:08:08,074 - DEBUG - < Connection: Upgrade
2025-06-28 01:08:08,074 - DEBUG - < Sec-WebSocket-Key: NYGKdE4qzA9wVO89I0ZTIA==
2025-06-28 01:08:08,075 - DEBUG - < Sec-WebSocket-Version: 13
2025-06-28 01:08:08,075 - DEBUG - < Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
2025-06-28 01:08:08,075 - DEBUG - < User-Agent: Python/3.10 websockets/10.4
2025-06-28 01:08:08,083 - DEBUG - > HTTP/1.1 101 Switching Protocols
2025-06-28 01:08:08,084 - DEBUG - > Upgrade: websocket
2025-06-28 01:08:08,084 - DEBUG - > Connection: Upgrade
2025-06-28 01:08:08,085 - DEBUG - > Sec-WebSocket-Accept: Oe0fwcOZGB5RuqEdf6lAMei0zzc=
2025-06-28 01:08:08,085 - DEBUG - > Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=12; client_max_window_bits=12
2025-06-28 01:08:08,085 - DEBUG - > Date: Fri, 27 Jun 2025 16:08:08 GMT
2025-06-28 01:08:08,085 - DEBUG - > Server: Python/3.10 websockets/10.4
2025-06-28 01:08:08,086 - INFO - connection open
2025-06-28 01:08:08,086 - DEBUG - = connection is OPEN
2025-06-28 01:08:08,087 - DEBUG - Initializing server 'SearchServer'
2025-06-28 01:08:08,104 - DEBUG - < TEXT '{"id": 1, "method": "initialize", "params": {}}' [47 bytes]
2025-06-28 01:08:08,105 - DEBUG - Received: {"id": 1, "method": "initialize", "params": {}}
2025-06-28 01:08:08,106 - DEBUG - Method: initialize
2025-06-28 01:08:08,106 - DEBUG - > TEXT '{"id": 1, "result": {"capabilities": {}}}' [41 bytes]
2025-06-28 01:08:08,116 - DEBUG - < TEXT '{"id": "call_lyzzmipu", "method": "add_numbers"...ams": {"a": 2, "b": 1}}' [76 bytes]
2025-06-28 01:08:08,118 - DEBUG - Received: {"id": "call_lyzzmipu", "method": "add_numbers", "params": {"a": 2, "b": 1}}
2025-06-28 01:08:08,122 - DEBUG - Method: add_numbers
2025-06-28 01:08:08,131 - DEBUG - Calling tool: add_numbers, arguments: {'a': 2, 'b': 1}
2025-06-28 01:08:08,133 - DEBUG - Addition result: 2 + 1 = 3
2025-06-28 01:08:08,133 - DEBUG - > TEXT '{"id": "call_lyzzmipu", "result": {"sum": 3}}' [45 bytes]
2025-06-28 01:08:08,137 - DEBUG - < CLOSE 1000 (OK) [2 bytes]
2025-06-28 01:08:08,137 - DEBUG - = connection is CLOSING
2025-06-28 01:08:08,137 - DEBUG - > CLOSE 1000 (OK) [2 bytes]
2025-06-28 01:08:08,139 - DEBUG - x half-closing TCP connection
2025-06-28 01:08:08,152 - DEBUG - = connection is CLOSED
2025-06-28 01:08:08,157 - INFO - connection closed
2025-06-28 01:10:36,342 - INFO - server closing
2025-06-28 01:10:36,344 - INFO - server closed
import asyncio
import os
import glob
import json
import websockets
from mcp.server import Server
import logging
import re
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('mcp_server.log'),
logging.StreamHandler()
]
)
class SearchServer(Server):
def __init__(self, directory):
super().__init__(name="SearchServer")
self.directory = os.path.expanduser(directory)
async def discover_tools(self):
return [
{
"name": "search_files",
"description": "Searches files in ~/Public using a single file name pattern (e.g., '*.txt', '*.pdf'). Call separately for multiple patterns. Invalid patterns (e.g., containing '/' or starting with '..') return an empty list.",
"parameters": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "A single file name pattern (e.g., '*.txt', '*.pdf'). Must not include path components (e.g., '~/Public/*.txt') or '/' nor start with '..' (e.g., '../*.txt'). Call separately for each pattern."
}
},
"required": ["pattern"]
}
},
{
"name": "add_numbers",
"description": "Adds two numbers and returns their sum (e.g., 3 + 5).",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "The first number (integer or float)."},
"b": {"type": "number", "description": "The second number (integer or float)."}
},
"required": ["a", "b"]
}
}
]
async def search_files(self, arguments):
logging.debug(f"Calling tool: search_files, arguments: {arguments}")
try:
# arguments の検証
if not isinstance(arguments, dict):
return {
"error": "Invalid arguments: must be a dictionary with a single 'pattern' key (e.g., {'pattern': '*.txt'}). Call search_files one pattern at a time."
}
if "pattern" not in arguments:
return {
"error": "Invalid arguments: 'pattern' key is required. Call search_files one pattern at a time (e.g., {'pattern': '*.txt'})."
}
pattern = arguments.get("pattern")
if not isinstance(pattern, str):
return {
"error": f"Invalid arguments: 'pattern' must be a string (e.g., '*.txt'). Got: {pattern}. Call search_files one pattern at a time."
}
if not re.match(r'^[a-zA-Z0-9*?.]+$', pattern) or "/" in pattern or pattern.startswith(".."):
logging.debug(f"Invalid pattern detected: {pattern}. Returning error.")
return {
"error": f"Invalid pattern: {pattern}. Pattern must not include '/' or start with '..'. Call search_files one pattern at a time."
}
search_path = os.path.join(self.directory, pattern)
logging.debug(f"Search path: {search_path}")
files = glob.glob(search_path, recursive=False)
return {"files": [os.path.basename(f) for f in files]}
except Exception as e:
logging.error(f"Error in search_files: {str(e)}")
return {
"error": f"Failed to process search_files: {str(e)}. Call search_files one pattern at a time with valid arguments (e.g., {'pattern': '*.txt'})."
}
async def add_numbers(self, arguments):
logging.debug(f"Calling tool: add_numbers, arguments: {arguments}")
try:
a = arguments.get("a", 0)
b = arguments.get("b", 0)
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
return {
"error": "Invalid arguments: 'a' and 'b' must be numbers."
}
result = a + b
logging.debug(f"Addition result: {a} + {b} = {result}")
return {"sum": result}
except Exception as e:
logging.error(f"Error in add_numbers: {str(e)}")
return {
"error": f"Failed to process add_numbers: {str(e)}."
}
async def websocket_handler(websocket):
server = SearchServer("~/Public")
async for message in websocket:
logging.debug(f"Received: {message}")
data = json.loads(message)
logging.debug(f"Method: {data.get('method') }")
if data.get("method") == "initialize":
response = {"id": data.get("id"), "result": {"capabilities": {}}}
await websocket.send(json.dumps(response))
elif data.get("method") == "discover_tools":
tools = await server.discover_tools()
response = {"id": data.get("id"), "result": tools}
await websocket.send(json.dumps(response))
elif data.get("method") == "search_files":
result = await server.search_files(data.get("params", {}))
response = {"id": data.get("id"), "result": result}
await websocket.send(json.dumps(response))
elif data.get("method") == "add_numbers":
result = await server.add_numbers(data.get("params", {}))
response = {"id": data.get("id"), "result": result}
await websocket.send(json.dumps(response))
else:
logging.error(f"Unknown method: {data.get('method')}")
async def main():
async with websockets.serve(websocket_handler, "localhost", 8080):
print("MCP server running at ws://localhost:8080")
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment