Skip to content

Commit 2c39072

Browse files
authored
Merge pull request #4 from nullchimp/mcp-integration
Mcp integration
2 parents 75e420e + 1c596c8 commit 2c39072

37 files changed

+3015
-957
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@ __pycache__
33
.env
44
.venv
55
.coverage
6+
.pytest_cache
7+
8+
config/*
9+
!config/mcp.template.json
610

711
coverage.xml

README.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AI Agent
22

3-
An intelligent AI agent framework written in Python, designed to facilitate seamless integration with Azure OpenAI services, file operations, web fetching, and search functionalities. This project provides modular components to build and extend AI-driven applications with best practices in testing, linting, and continuous integration.
3+
An intelligent AI agent framework written in Python, designed to facilitate seamless integration with Model Context Protocol (MCP) servers, Azure OpenAI services, file operations, web fetching, and search functionalities. This project provides modular components to build and extend AI-driven applications with best practices in testing, linting, and continuous integration.
44

55
## Table of Contents
66
- [Features](#features)
@@ -13,25 +13,30 @@ An intelligent AI agent framework written in Python, designed to facilitate seam
1313
- [License](#license)
1414

1515
## Features
16-
- Integration with Azure OpenAI for chat and completion services
16+
- Integration with Model Context Protocol (MCP) servers for AI tool execution
17+
- Support for Azure OpenAI for chat and completion services
1718
- Modular file operations (read, write, list)
1819
- Web fetching and conversion utilities
1920
- Search client with pluggable backends
2021
- Tooling for codegen workflows
21-
- Configurable via environment variables
22+
- Configurable via environment variables and JSON configuration files
2223

2324
## Architecture
2425
The codebase follows a modular structure under `src/`:
2526

2627
```
2728
src/
2829
├── agent.py # Entry point for the AI agent
30+
├── chat.py # Chat interface implementation
31+
├── main.py # Main application entry point
2932
├── libs/ # Core libraries and abstractions
30-
│ ├── azureopenai/ # Azure OpenAI wrappers (chat, client)
3133
│ ├── fileops/ # File operations utilities
3234
│ ├── search/ # Search client and service
3335
│ └── webfetch/ # Web fetching and conversion services
34-
└── tools/ # Command-line tools for file and web operations
36+
├── tools/ # Command-line tools for file, web operations and more
37+
└── utils/ # Utility modules
38+
├── azureopenai/ # Azure OpenAI wrappers (chat, client)
39+
└── mcpclient/ # MCP client for server interactions
3540
```
3641

3742
## Installation
@@ -44,17 +49,22 @@ src/
4449
2. Create and activate a Python 3.9+ virtual environment:
4550
```bash
4651
python3 -m venv .venv
47-
source .venv/bin/activate # On Windows: .venv\\Scripts\\activate
52+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
4853
```
4954
3. Install dependencies:
5055
```bash
5156
pip install -r requirements.txt
5257
```
53-
4. Copy `.env.example` to `.env` and configure your Azure OpenAI credentials:
58+
4. Copy `.env.example` to `.env` and configure your credentials:
5459
```bash
5560
cp .env.example .env
5661
# Edit .env to set environment variables
5762
```
63+
5. Configure MCP servers (optional):
64+
```bash
65+
cp config/mcp.template.json config/mcp.json
66+
# Edit the config/mcp.json file to configure your MCP servers
67+
```
5868

5969
## Usage
6070

@@ -68,7 +78,14 @@ Run the **AI Agent** with:
6878
python -m src.agent
6979
```
7080

71-
Customize behavior via environment variables defined in `.env`.
81+
Run the **Main Application** with:
82+
```bash
83+
python -m src.main
84+
```
85+
86+
Customize behavior via:
87+
- Environment variables defined in `.env`
88+
- MCP server configurations in `config/mcp.json`
7289

7390
## Development
7491

@@ -91,14 +108,14 @@ mypy src
91108

92109
## Testing
93110

94-
All changes must be validated with tests.
111+
All changes must be validated with tests. The `tests/` directory mirrors the structure of `src/`.
95112

96113
Run unit and integration tests with coverage:
97114
```bash
98115
pytest --cov=src
99116
```
100117

101-
Ensure 100% pass before committing.
118+
Ensure all tests pass before committing.
102119

103120
## Contributing
104121

config/mcp.template.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"servers": {
3+
"<Server Name>": {
4+
"command": "<Command to run the server>",
5+
"args": [
6+
"<Arguments to pass to the server>"
7+
],
8+
"env": {
9+
"<Environment Variable Name>": "<Environment Variable Value>"
10+
}
11+
}
12+
}
13+
}

src/agent.py

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from dotenv import load_dotenv
22
load_dotenv()
33

4-
import json
5-
from typing import Dict, Any, List, Generator
4+
from datetime import date
65

7-
from utils import chatloop
6+
import json
7+
from typing import Dict, Any, Generator, List
88

9+
from utils import chatutil, graceful_exit
910
from utils.azureopenai.chat import Chat
11+
from tools import Tool
1012
from tools.google_search import GoogleSearch
1113
from tools.read_file import ReadFile
1214
from tools.write_file import WriteFile
@@ -20,38 +22,21 @@
2022
"list_files": ListFiles(),
2123
"web_fetch": WebFetch()
2224
}
25+
chat = Chat.create(tool_map)
26+
def add_tool(tool: Tool) -> None:
27+
tool_map[tool.name] = tool
28+
chat.add_tool(tool)
2329

24-
def process_tool_calls(response: Dict[str, Any]) -> Generator[Dict[str, Any], None, None]:
25-
"""Process tool calls from the LLM response and return results.
26-
27-
Args:
28-
response: The response from the LLM containing tool calls.
29-
30-
Yields:
31-
Dict with tool response information.
32-
"""
33-
# Handle case where tool_calls is None or not present
34-
if not response or not response.get("tool_calls") or not isinstance(response.get("tool_calls"), list):
35-
return
36-
30+
async def process_tool_calls(response: Dict[str, Any], call_back) -> None:
3731
for tool_call in response.get("tool_calls", []):
38-
if not isinstance(tool_call, dict):
39-
continue
40-
41-
tool_id = tool_call.get("id", "unknown_tool")
42-
43-
# Extract function data, handling possible missing keys
4432
function_data = tool_call.get("function", {})
45-
if not isinstance(function_data, dict):
46-
continue
47-
48-
tool_name = function_data.get("name")
33+
tool_name = function_data.get("name", "")
4934
if not tool_name:
5035
continue
5136

5237
arguments = function_data.get("arguments", "{}")
5338

54-
print(f"<Tool: {tool_name}>")
39+
print(f"<Tool: {tool_name}> ", arguments)
5540

5641
try:
5742
args = json.loads(arguments)
@@ -65,20 +50,22 @@ def process_tool_calls(response: Dict[str, Any]) -> Generator[Dict[str, Any], No
6550
if tool_name in tool_map:
6651
tool_instance = tool_map[tool_name]
6752
try:
68-
tool_result = tool_instance.run(**args)
53+
tool_result = await tool_instance.run(**args)
54+
print(f"<Tool Result: {tool_name}> ", tool_result)
6955
except Exception as e:
7056
tool_result = {
7157
"error": f"Error running tool '{tool_name}': {str(e)}"
7258
}
59+
print(f"<Tool Error: {tool_name}> ", tool_result)
7360

74-
yield {
61+
call_back({
7562
"role": "tool",
76-
"tool_call_id": tool_id,
63+
"tool_call_id": tool_call.get("id", "unknown_tool"),
7764
"content": json.dumps(tool_result)
78-
}
65+
})
7966

8067
# Define enhanced system role with instructions on using all available tools
81-
system_role = """
68+
system_role = f"""
8269
You are a helpful assistant.
8370
Your Name is Agent Smith and you have access to various capabilities:
8471
@@ -90,44 +77,35 @@ def process_tool_calls(response: Dict[str, Any]) -> Generator[Dict[str, Any], No
9077
9178
Use these tools appropriately to provide comprehensive assistance.
9279
Synthesize and cite your sources correctly when using search or web content.
80+
81+
Today is {date.today().strftime("%d %B %Y")}.
9382
"""
9483

95-
chat = Chat.create(tool_map)
9684
messages = [{"role": "system", "content": system_role}]
9785

98-
@chatloop("Agent")
99-
async def run_conversation(user_prompt):
86+
@graceful_exit
87+
@chatutil("Agent")
88+
async def run_conversation(user_prompt) -> str:
10089
# Example:
10190
# user_prompt = """
10291
# Who is the current chancellor of Germany?
10392
# Write the result to a file with the name 'chancellor.txt' in a folder with the name 'docs'.
10493
# Then list me all files in my root directory and put the result in another file called 'list.txt' in the same 'docs' folder.
10594
# """
106-
95+
10796
messages.append({"role": "user", "content": user_prompt})
10897
response = await chat.send_messages(messages)
109-
110-
# Handle possible None response
111-
if not response:
112-
return ""
113-
114-
# Handle missing or empty choices
11598
choices = response.get("choices", [])
116-
if not choices:
117-
return ""
11899

119100
assistant_message = choices[0].get("message", {})
120101
messages.append(assistant_message)
121102

122103
# Handle the case where tool_calls might be missing or not a list
123104
while assistant_message.get("tool_calls"):
124-
for result in process_tool_calls(assistant_message):
125-
messages.append(result)
105+
await process_tool_calls(assistant_message, messages.append)
126106

127107
response = await chat.send_messages(messages)
128-
129-
# Handle possible None response or missing choices
130-
if not response or not response.get("choices"):
108+
if not (response and response.get("choices", None)):
131109
break
132110

133111
assistant_message = response.get("choices", [{}])[0].get("message", {})
@@ -136,4 +114,5 @@ async def run_conversation(user_prompt):
136114
return assistant_message.get("content", "")
137115

138116
if __name__ == "__main__":
139-
run_conversation()
117+
import asyncio
118+
asyncio.run(run_conversation())

src/chat.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
from dotenv import load_dotenv
22
load_dotenv()
33

4-
from typing import Dict, Any, Optional
5-
6-
from utils import chatloop
4+
from utils import chatutil, graceful_exit, mainloop
75
from utils.azureopenai.chat import Chat
86

97
# Initialize the Chat client
@@ -20,7 +18,9 @@
2018

2119
messages = [{"role": "system", "content": system_role}]
2220

23-
@chatloop("Chat")
21+
@mainloop
22+
@graceful_exit
23+
@chatutil("Chat")
2424
async def run_conversation(user_prompt: str) -> str:
2525
"""Run a conversation with the user.
2626
@@ -50,4 +50,5 @@ async def run_conversation(user_prompt: str) -> str:
5050
return content
5151

5252
if __name__ == "__main__":
53-
run_conversation()
53+
import asyncio
54+
asyncio.run(run_conversation())

src/main.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
import asyncio
2+
import agent
23

3-
import agent, chat
4+
from utils import graceful_exit, mainloop
5+
from utils.mcpclient.sessions_manager import MCPSessionManager
46

5-
async def process_one():
6-
while True:
7-
print("Processing one...")
8-
await asyncio.sleep(1)
7+
session_manager = MCPSessionManager()
98

10-
async def process_two():
9+
@graceful_exit
10+
async def mcp_discovery():
11+
success = await session_manager.load_mcp_sessions()
12+
if not success:
13+
print("No valid MCP sessions found in configuration")
14+
return
15+
16+
await session_manager.list_tools()
17+
for tool in session_manager.tools:
18+
agent.add_tool(tool)
19+
20+
@mainloop
21+
@graceful_exit
22+
async def agent_task():
1123
await agent.run_conversation()
1224

25+
@graceful_exit
1326
async def main():
14-
# Run both coroutines concurrently
15-
await asyncio.gather(
16-
process_one(),
17-
process_two()
18-
)
27+
print("<Discovery: MCP Server>")
28+
await mcp_discovery()
29+
print("\n" + "-" * 50 + "\n")
30+
31+
for server_name in session_manager.sessions.keys():
32+
print(f"<Active MCP Server: {server_name}>")
33+
34+
await agent_task()
1935

2036
if __name__ == "__main__":
2137
asyncio.run(main())

0 commit comments

Comments
 (0)