iflow-mcp_lukeage-python-pip-mcp 0.1.0__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.
- iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/METADATA +40 -0
- iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/RECORD +7 -0
- iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/WHEEL +4 -0
- iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- mcp_client.py +149 -0
- mcp_server.py +72 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iflow-mcp_lukeage-python-pip-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal Example Implementation of an Anthropic MCP Server in Python with Pip
|
|
5
|
+
Project-URL: Homepage, https://github.com/lukeage/python-pip-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/lukeage/python-pip-mcp
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: anthropic>=0.15.0
|
|
10
|
+
Requires-Dist: mcp>=0.1.0
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# python-pip-mcp
|
|
15
|
+
|
|
16
|
+
Minimal Example Implementation of an Anthropic [MCP](https://modelcontextprotocol.io/introduction) Client and Server in Python with Pip.
|
|
17
|
+
|
|
18
|
+
The goal of this repository is to provide a reference implementation of a **mcp client and server** that can be easily debugged in VSCode on Windows using the Python / Python Debugger extension.
|
|
19
|
+
|
|
20
|
+
Alternative IDEs and OS are not tested but should work with minimal adjustments, too.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
# create venv
|
|
26
|
+
python -m venv myenv
|
|
27
|
+
myenv\Scripts\activate
|
|
28
|
+
|
|
29
|
+
# install requirements
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
|
|
32
|
+
# create a .env file and set your anthropic api key
|
|
33
|
+
cp .env.sample .env
|
|
34
|
+
|
|
35
|
+
# run mcp_client.py script
|
|
36
|
+
python mcp_client.py
|
|
37
|
+
|
|
38
|
+
# query for current time
|
|
39
|
+
```
|
|
40
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mcp_client.py,sha256=RH5rYC2KnsIMKnJlszCU8Y_1XoVtiglEbzCPqlyb-4g,4897
|
|
2
|
+
mcp_server.py,sha256=0jbowV3SwKxkZnO3KyuTcbWR1lzHoy9bv1alxxOozco,2075
|
|
3
|
+
iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/METADATA,sha256=yB60IlfN_X4RMSP90IngzxDc17GjsVS2gZqsrsaCuvc,1213
|
|
4
|
+
iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
+
iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/entry_points.txt,sha256=WYlei30NJFPcCHQvEjOhREZhnCmL7om-EyNcwxRyP1c,48
|
|
6
|
+
iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=7UhW6GDE56ZAQopiE5nNZGOr-xez3mMm0sIfIcT_ewk,1067
|
|
7
|
+
iflow_mcp_lukeage_python_pip_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Lukas Hain
|
|
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.
|
mcp_client.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# mcp_client.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from contextlib import AsyncExitStack
|
|
7
|
+
|
|
8
|
+
from mcp import ClientSession, StdioServerParameters
|
|
9
|
+
from mcp.client.stdio import stdio_client
|
|
10
|
+
|
|
11
|
+
from anthropic import Anthropic
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
load_dotenv() # load environment variables from .env
|
|
15
|
+
|
|
16
|
+
class MCPClient:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
# Initialize session and client objects
|
|
19
|
+
self.session: Optional[ClientSession] = None
|
|
20
|
+
self.exit_stack = AsyncExitStack()
|
|
21
|
+
self.anthropic = Anthropic()
|
|
22
|
+
|
|
23
|
+
async def connect_to_server(self, server_script_path: str):
|
|
24
|
+
"""Connect to an MCP server"""
|
|
25
|
+
# Get the current Python interpreter path
|
|
26
|
+
python_path = sys.executable
|
|
27
|
+
|
|
28
|
+
server_params = StdioServerParameters(
|
|
29
|
+
command=python_path,
|
|
30
|
+
args=[server_script_path]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
|
34
|
+
self.stdio, self.write = stdio_transport
|
|
35
|
+
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
|
|
36
|
+
|
|
37
|
+
await self.session.initialize()
|
|
38
|
+
|
|
39
|
+
# List available tools
|
|
40
|
+
response = await self.session.list_tools()
|
|
41
|
+
tools = response.tools
|
|
42
|
+
print("\nConnected to server with tools:", [tool.name for tool in tools])
|
|
43
|
+
|
|
44
|
+
async def process_query(self, query: str) -> str:
|
|
45
|
+
"""Process a query using Claude and available tools"""
|
|
46
|
+
messages = [
|
|
47
|
+
{
|
|
48
|
+
"role": "user",
|
|
49
|
+
"content": query
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
response = await self.session.list_tools()
|
|
54
|
+
available_tools = [{
|
|
55
|
+
"name": tool.name,
|
|
56
|
+
"description": tool.description,
|
|
57
|
+
"input_schema": tool.inputSchema
|
|
58
|
+
} for tool in response.tools]
|
|
59
|
+
|
|
60
|
+
# Initial Claude API call
|
|
61
|
+
response = self.anthropic.messages.create(
|
|
62
|
+
model="claude-3-sonnet-20240229",
|
|
63
|
+
max_tokens=1000,
|
|
64
|
+
messages=messages,
|
|
65
|
+
tools=available_tools
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Process response and handle tool calls
|
|
69
|
+
tool_results = []
|
|
70
|
+
final_text = []
|
|
71
|
+
|
|
72
|
+
for content in response.content:
|
|
73
|
+
if content.type == 'text':
|
|
74
|
+
final_text.append(content.text)
|
|
75
|
+
elif content.type == 'tool_use':
|
|
76
|
+
tool_name = content.name
|
|
77
|
+
tool_args = content.input
|
|
78
|
+
|
|
79
|
+
# Execute tool call
|
|
80
|
+
result = await self.session.call_tool(tool_name, tool_args)
|
|
81
|
+
tool_results.append({"call": tool_name, "result": result})
|
|
82
|
+
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
|
|
83
|
+
|
|
84
|
+
# Continue conversation with tool results
|
|
85
|
+
if hasattr(content, 'text') and content.text:
|
|
86
|
+
messages.append({
|
|
87
|
+
"role": "assistant",
|
|
88
|
+
"content": content.text
|
|
89
|
+
})
|
|
90
|
+
messages.append({
|
|
91
|
+
"role": "user",
|
|
92
|
+
"content": f"Tool {tool_name} returned: {result.content[0].text}"
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# Get next response from Claude
|
|
96
|
+
response = self.anthropic.messages.create(
|
|
97
|
+
model="claude-3-5-sonnet-latest",
|
|
98
|
+
max_tokens=1000,
|
|
99
|
+
messages=messages,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
final_text.append(response.content[0].text)
|
|
103
|
+
|
|
104
|
+
return "\n".join(final_text)
|
|
105
|
+
|
|
106
|
+
async def chat_loop(self):
|
|
107
|
+
"""Run an interactive chat loop"""
|
|
108
|
+
print("\nMCP Client Started!")
|
|
109
|
+
print("Type your queries or 'quit' to exit.")
|
|
110
|
+
|
|
111
|
+
while True:
|
|
112
|
+
try:
|
|
113
|
+
query = input("\nQuery: ").strip()
|
|
114
|
+
|
|
115
|
+
if query.lower() == 'quit':
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
response = await self.process_query(query)
|
|
119
|
+
print("\n" + response)
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"\nError: {str(e)}")
|
|
123
|
+
|
|
124
|
+
async def cleanup(self):
|
|
125
|
+
"""Clean up resources"""
|
|
126
|
+
await self.exit_stack.aclose()
|
|
127
|
+
|
|
128
|
+
async def main():
|
|
129
|
+
if len(sys.argv) < 2:
|
|
130
|
+
print("Usage: python client.py <path_to_server_script>")
|
|
131
|
+
server_path = "./mcp_server.py"
|
|
132
|
+
else:
|
|
133
|
+
server_path = sys.argv[1]
|
|
134
|
+
|
|
135
|
+
# Set binary mode for stdin/stdout on Windows
|
|
136
|
+
if sys.platform == 'win32':
|
|
137
|
+
import msvcrt
|
|
138
|
+
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
|
139
|
+
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
140
|
+
|
|
141
|
+
client = MCPClient()
|
|
142
|
+
try:
|
|
143
|
+
await client.connect_to_server(server_path)
|
|
144
|
+
await client.chat_loop()
|
|
145
|
+
finally:
|
|
146
|
+
await client.cleanup()
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
asyncio.run(main())
|
mcp_server.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# mcp_server.py
|
|
2
|
+
import asyncio
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
from mcp.server import Server, NotificationOptions
|
|
7
|
+
from mcp.server.models import InitializationOptions
|
|
8
|
+
import mcp.server.stdio
|
|
9
|
+
import mcp.types as types
|
|
10
|
+
|
|
11
|
+
# Create server instance
|
|
12
|
+
server = Server("date-server")
|
|
13
|
+
|
|
14
|
+
@server.list_tools()
|
|
15
|
+
async def handle_list_tools() -> list[types.Tool]:
|
|
16
|
+
"""List available tools"""
|
|
17
|
+
return [
|
|
18
|
+
types.Tool(
|
|
19
|
+
name="get-datetime",
|
|
20
|
+
description="Get the current date and time",
|
|
21
|
+
inputSchema={
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {}, # No input needed
|
|
24
|
+
"required": []
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
@server.call_tool()
|
|
30
|
+
async def handle_call_tool(
|
|
31
|
+
name: str,
|
|
32
|
+
arguments: dict | None
|
|
33
|
+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
34
|
+
"""Handle tool execution"""
|
|
35
|
+
if name == "get-datetime":
|
|
36
|
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
37
|
+
return [
|
|
38
|
+
types.TextContent(
|
|
39
|
+
type="text",
|
|
40
|
+
text=f"Current date and time: {current_time}"
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
44
|
+
|
|
45
|
+
async def main():
|
|
46
|
+
"""Run the server"""
|
|
47
|
+
# Set binary mode for stdin/stdout on Windows
|
|
48
|
+
if sys.platform == 'win32':
|
|
49
|
+
import msvcrt
|
|
50
|
+
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
|
51
|
+
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
52
|
+
|
|
53
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
54
|
+
await server.run(
|
|
55
|
+
read_stream,
|
|
56
|
+
write_stream,
|
|
57
|
+
InitializationOptions(
|
|
58
|
+
server_name="date-server",
|
|
59
|
+
server_version="0.1.0",
|
|
60
|
+
capabilities=server.get_capabilities(
|
|
61
|
+
notification_options=NotificationOptions(),
|
|
62
|
+
experimental_capabilities={},
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
|
|
70
|
+
def cli():
|
|
71
|
+
"""CLI entry point for setuptools"""
|
|
72
|
+
asyncio.run(main())
|