Skip to content

Commit 9ff3a85

Browse files
Merge pull request #28 from insightbuilder/multi-server-client
ready with multi server client
2 parents e5c8c3c + de40004 commit 9ff3a85

2 files changed

Lines changed: 296 additions & 2 deletions

File tree

docs/mcp_excel_server.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ After the client start, you will be prompted for the query.
4646
Query: Who are you?.
4747
```
4848

49-
### Available MCP Server Tools:
49+
### Available Excel MCP Server Tools:
5050

5151
These tool are using mcp resources that are
5252
manipulating and analysing the excel files.
@@ -67,12 +67,50 @@ manipulating and analysing the excel files.
6767
file
6868
- remove_file: Removes a file from the system
6969

70-
The mcp client in this proect is enabled with
70+
#### Connecting SQL DB with MCP Client:
71+
72+
The mcp client in this project is enabled with
7173
SQLite db connection. The same has been discussed
7274
in this [video](https://youtu.be/FAMg9kZpMQw)
7375

76+
#### Multi Server Connecting with MCP Client:
77+
78+
Multi_server_client.py script is implemented to
79+
connect with multiple mcp servers. Follow the.
80+
below command for executing the script when you
81+
are connecting with multiple python mcp server.
82+
83+
Note: The supporting packages for the server files
84+
has to be made available either through uv or
85+
venv. Else the server tools cannot execute.
86+
87+
```
88+
uv run Multi_server_client.py server1.py
89+
server2.py
90+
```
91+
92+
If you are connecting with Javascript mcp server,
93+
then you need to have the node modules installed
94+
in the same folder where you are executing the mcp
95+
client.
96+
97+
In case of the JS servers, the server file needs
98+
to be usually compiled using npm run build
99+
command. Refer to the documentation of the server
100+
for more details.
101+
102+
The execution of the script can be done as below.
103+
104+
```
105+
uv run Multi_server_client.py server1.py
106+
index.js
107+
```
108+
109+
### Testing with mcp inspector:
110+
74111
When you need use mcp inspector to debug the code,
75112
use the command below. Ensure you have
113+
76114
[npx](https://docs.npmjs.com/cli/v8/commands/npx)
77115
and [node](https://nodejs.org/en/download)
78116
installed
@@ -91,6 +129,12 @@ server.py
91129
- Notion Code referred from:
92130
[Notion Example code](../fw_ex/notionapi_spike/)
93131

132+
- GMail MCP Server :
133+
[Gmail Server](https://github.com/GongRzhe/Gmail-MCP-Server)
134+
135+
````
136+
94137
```
95138
96139
```
140+
````
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# /// script
2+
# requires-python = ">=3.11"
3+
# dependencies = [
4+
# "anthropic",
5+
# "mcp",
6+
# "openpyxl",
7+
# "python-dotenv",
8+
# ]
9+
# ///
10+
import asyncio
11+
from typing import Optional, List
12+
from contextlib import AsyncExitStack
13+
14+
# pyright: reportMissingImports=false
15+
# pyright: reportOptionalSubscript=false
16+
17+
from mcp import ClientSession, StdioServerParameters
18+
from mcp.client.stdio import stdio_client
19+
20+
from anthropic import Anthropic
21+
from dotenv import load_dotenv
22+
23+
# uncomment this when running in your local environment
24+
# ensure you have updated the .env file with the Anthropic API Key
25+
# load_dotenv() # load environment variables from .env
26+
27+
28+
class MCPClient:
29+
def __init__(self):
30+
# Initialize session and client objects
31+
self.sessions: Optional[List[ClientSession]] = []
32+
self.exit_stack = AsyncExitStack()
33+
self.anthropic = Anthropic()
34+
35+
async def connect_to_servers(self, server_script_paths: List[str]):
36+
for server_script_path in server_script_paths:
37+
await self.connect_to_server(server_script_path)
38+
39+
# List available tools
40+
for idx, loc in enumerate(self.sessions):
41+
response = await loc.list_tools()
42+
loc_tools = response.tools
43+
print(
44+
f"\nConnected to {server_script_paths[idx]} server with tools:",
45+
[tool.name for tool in loc_tools],
46+
)
47+
48+
# methods will go here
49+
async def connect_to_server(self, server_script_path: str):
50+
"""Connect to notion MCP server
51+
52+
Args:
53+
server_script_path: Path to the server script (.py or .js)
54+
"""
55+
is_python = server_script_path.endswith(".py")
56+
is_js = server_script_path.endswith(".js")
57+
if not (is_python or is_js):
58+
raise ValueError("Server script must be a .py or .js file")
59+
60+
command = "python" if is_python else "node"
61+
server_params = StdioServerParameters(
62+
command=command, args=[server_script_path], env=None
63+
)
64+
65+
stdio_transport = await self.exit_stack.enter_async_context(
66+
stdio_client(server_params)
67+
)
68+
self.stdio, self.write = stdio_transport
69+
70+
loc_session = await self.exit_stack.enter_async_context(
71+
ClientSession(self.stdio, self.write)
72+
)
73+
# append the session to list of sessions
74+
self.sessions.append(loc_session)
75+
76+
await self.sessions[-1].initialize()
77+
78+
async def create_or_connect_db(self):
79+
import sqlite3
80+
81+
# Connect to SQLite (creates a file called chatbot.db)
82+
conn = sqlite3.connect("mcphistory.db")
83+
cursor = conn.cursor()
84+
85+
# Create a table to store query-response pairs
86+
cursor.execute("""
87+
CREATE TABLE IF NOT EXISTS conversations (
88+
id INTEGER PRIMARY KEY AUTOINCREMENT,
89+
query TEXT NOT NULL,
90+
response TEXT NOT NULL
91+
)
92+
""")
93+
94+
conn.commit()
95+
conn.close()
96+
97+
async def save_history(self, query: str, response: str):
98+
import sqlite3
99+
100+
# Connect to SQLite (creates a file called chatbot.db)
101+
conn = sqlite3.connect("mcphistory.db")
102+
cursor = conn.cursor()
103+
104+
# Insert the query-response pair into the table
105+
cursor.execute(
106+
"INSERT INTO conversations (query, response) VALUES (?, ?)",
107+
(query, response),
108+
)
109+
110+
conn.commit()
111+
conn.close()
112+
113+
async def read_history(self):
114+
import sqlite3
115+
116+
# Connect to SQLite (creates a file called chatbot.db)
117+
conn = sqlite3.connect("mcphistory.db")
118+
cursor = conn.cursor()
119+
120+
# Insert the query-response pair into the table
121+
cursor.execute("SELECT query, response FROM conversations")
122+
123+
history = ""
124+
125+
rows = cursor.fetchall()
126+
for row in rows:
127+
history += f"Query: {row[0]}\nResponse: {row[1]}\n{history}\n"
128+
129+
return history
130+
131+
async def process_query(self, query: str) -> str:
132+
"""Process a query using Claude and available tools"""
133+
134+
hal_system = """You are hal3025, an expert in working on filesystem and excel sheets.
135+
You have access to a local filesystem with read and write access.
136+
You are very good in analysing xlsx files and you can use the available
137+
tools with you and return the results to the user.
138+
When user asks you to refer to past conversation or history, then refer to
139+
the query and response available with you and respond. Don't say you
140+
do not have access to history.
141+
Just use the tools, and provide the updates the tools are giving.
142+
Do not use external python packages for the analysis.
143+
Use the tools that are available to you.
144+
Do not apologize. Do not provide guidance or examples. Do not share what you cannot do.
145+
Please do not provide the python code for the user requests.
146+
Do not explain how something can be done."""
147+
148+
messages = [{"role": "user", "content": query}]
149+
available_tools = []
150+
for session in self.sessions:
151+
response = await session.list_tools()
152+
available_tools.extend([
153+
{
154+
"name": tool.name,
155+
"description": tool.description,
156+
"input_schema": tool.inputSchema,
157+
}
158+
for tool in response.tools
159+
])
160+
161+
# Initial Claude API call
162+
response = self.anthropic.messages.create(
163+
model="claude-3-5-haiku-20241022",
164+
max_tokens=1000,
165+
system=hal_system,
166+
messages=messages,
167+
tools=available_tools,
168+
)
169+
170+
# Process response and handle tool calls
171+
tool_results = []
172+
final_text = []
173+
174+
for content in response.content: # each tool call will handle seperately
175+
if content.type == "text":
176+
final_text.append(content.text)
177+
elif content.type == "tool_use":
178+
tool_name = content.name
179+
tool_args = content.input
180+
181+
# Execute tool call
182+
result = await self.session.call_tool(tool_name, tool_args)
183+
tool_results.append({"call": tool_name, "result": result})
184+
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
185+
186+
# Continue conversation with tool results
187+
if hasattr(content, "text") and content.text:
188+
messages.append({"role": "assistant", "content": content.text})
189+
messages.append({"role": "user", "content": result.content})
190+
191+
# Get next response from Claude
192+
response = self.anthropic.messages.create(
193+
model="claude-3-5-haiku-20241022",
194+
system=hal_system,
195+
max_tokens=1000,
196+
messages=messages,
197+
)
198+
199+
final_text.append(response.content[0].text)
200+
return "\n".join(final_text)
201+
202+
async def chat_loop(self):
203+
"""Run an interactive chat loop"""
204+
print("\nMCP Client Started!")
205+
print("Type your queries or 'quit' to exit.")
206+
history = await self.read_history()
207+
while True:
208+
try:
209+
query = input("\nInteract with Excel File here: ").strip()
210+
# below the history is assembled
211+
print(history)
212+
query_with_history = (
213+
f"Previous conversation:\n{history}\n Query: {query}"
214+
)
215+
if query.lower() == "quit":
216+
break
217+
218+
response = await self.process_query(query_with_history)
219+
# here is response is apended to history
220+
await self.save_history(query, response)
221+
print("\n" + response)
222+
223+
except Exception as e:
224+
print(f"\nError: {str(e)}")
225+
226+
async def cleanup(self):
227+
"""Clean up resources"""
228+
await self.exit_stack.aclose()
229+
230+
231+
async def main():
232+
if len(sys.argv) < 3:
233+
# we use uv package manager so uv run mcpclient.py mcpserver.py
234+
print("Usage: uv run mcpclient.py server1.py server2.js")
235+
sys.exit(1)
236+
237+
client = MCPClient()
238+
239+
try:
240+
await client.create_or_connect_db()
241+
await client.connect_to_servers(sys.argv[1:])
242+
await client.chat_loop()
243+
finally:
244+
await client.cleanup()
245+
246+
247+
if __name__ == "__main__":
248+
import sys
249+
250+
asyncio.run(main())

0 commit comments

Comments
 (0)