finta-aurora-mcp 1.0.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.
- finta_aurora_mcp/__init__.py +3 -0
- finta_aurora_mcp/auth.py +105 -0
- finta_aurora_mcp/fix_org_info.py +44 -0
- finta_aurora_mcp/mcp.py +498 -0
- finta_aurora_mcp-1.0.0.dist-info/METADATA +216 -0
- finta_aurora_mcp-1.0.0.dist-info/RECORD +19 -0
- finta_aurora_mcp-1.0.0.dist-info/WHEEL +5 -0
- finta_aurora_mcp-1.0.0.dist-info/top_level.txt +2 -0
- tools/__init__.py +23 -0
- tools/addInvestorToTrackerTool.py +114 -0
- tools/contactSupportTool.py +7 -0
- tools/editInvestorTool.py +653 -0
- tools/getInvestorTool.py +109 -0
- tools/imageTool.py +77 -0
- tools/pdfScraperTool.py +31 -0
- tools/pineconeKnowledgeTool.py +35 -0
- tools/pineconeResourceTool.py +27 -0
- tools/serpAPITool.py +16 -0
- tools/webScraperTool.py +30 -0
finta_aurora_mcp/auth.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Aurora MCP Authentication
|
|
4
|
+
Simple OAuth login - opens browser and stores token.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import webbrowser
|
|
10
|
+
import http.server
|
|
11
|
+
import socketserver
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import json
|
|
14
|
+
import requests
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
FINTA_API_URL_STAGING = "https://us-central1-equity-token-stage.cloudfunctions.net"
|
|
18
|
+
FINTA_API_URL_PRODUCTION = "https://us-central1-equity-token.cloudfunctions.net"
|
|
19
|
+
|
|
20
|
+
def get_api_url():
|
|
21
|
+
"""Get API URL based on environment."""
|
|
22
|
+
use_staging = os.getenv("FINTA_STAGING", "true").lower() == "true"
|
|
23
|
+
return FINTA_API_URL_STAGING if use_staging else FINTA_API_URL_PRODUCTION
|
|
24
|
+
|
|
25
|
+
def get_token_path():
|
|
26
|
+
"""Get path to store token."""
|
|
27
|
+
return Path.home() / ".cursor" / "aurora_token.json"
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
print("Aurora MCP Authentication")
|
|
31
|
+
print("=" * 40)
|
|
32
|
+
print("Opening browser for login...\n")
|
|
33
|
+
|
|
34
|
+
api_url = get_api_url()
|
|
35
|
+
auth_url = (
|
|
36
|
+
f"{api_url}/auroraOAuthAuthorize?"
|
|
37
|
+
f"response_type=code&"
|
|
38
|
+
f"client_id=cursor&"
|
|
39
|
+
f"redirect_uri=http://localhost:8765/callback"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
webbrowser.open(auth_url)
|
|
43
|
+
|
|
44
|
+
class CallbackHandler(http.server.SimpleHTTPRequestHandler):
|
|
45
|
+
def do_GET(self):
|
|
46
|
+
query = urllib.parse.urlparse(self.path).query
|
|
47
|
+
params = urllib.parse.parse_qs(query)
|
|
48
|
+
|
|
49
|
+
if 'code' in params:
|
|
50
|
+
code = params['code'][0]
|
|
51
|
+
api_url = get_api_url()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
resp = requests.post(
|
|
55
|
+
f"{api_url}/auroraOAuthToken",
|
|
56
|
+
json={
|
|
57
|
+
"grant_type": "authorization_code",
|
|
58
|
+
"code": code,
|
|
59
|
+
"redirect_uri": "http://localhost:8765/callback"
|
|
60
|
+
},
|
|
61
|
+
timeout=10
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if resp.status_code == 200:
|
|
65
|
+
token_data = resp.json()
|
|
66
|
+
access_token = token_data.get("access_token")
|
|
67
|
+
|
|
68
|
+
token_path = get_token_path()
|
|
69
|
+
token_path.parent.mkdir(exist_ok=True)
|
|
70
|
+
with open(token_path, 'w') as f:
|
|
71
|
+
json.dump(token_data, f, indent=2)
|
|
72
|
+
|
|
73
|
+
self.send_response(200)
|
|
74
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
75
|
+
self.end_headers()
|
|
76
|
+
html = """<html><body style="font-family: system-ui; padding: 40px; text-align: center;"><h1>Authentication Successful!</h1><p>Token has been stored.</p><p>You can close this window and restart Cursor.</p></body></html>"""
|
|
77
|
+
self.wfile.write(html.encode('utf-8'))
|
|
78
|
+
print("\n✓ Authentication successful!")
|
|
79
|
+
print(f"✓ Token stored at: {token_path}")
|
|
80
|
+
print("\nRestart Cursor to use Aurora MCP.")
|
|
81
|
+
return
|
|
82
|
+
else:
|
|
83
|
+
print(f"\n✗ Failed to get token: {resp.status_code} - {resp.text}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"\n✗ Error: {e}")
|
|
86
|
+
|
|
87
|
+
self.send_response(200)
|
|
88
|
+
self.send_header('Content-type', 'text/html')
|
|
89
|
+
self.end_headers()
|
|
90
|
+
self.wfile.write(b"<h1>Processing...</h1>")
|
|
91
|
+
|
|
92
|
+
def log_message(self, *args):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
with socketserver.TCPServer(("", 8765), CallbackHandler) as httpd:
|
|
96
|
+
print("Waiting for authentication callback...")
|
|
97
|
+
print("(Press Ctrl+C to cancel)\n")
|
|
98
|
+
try:
|
|
99
|
+
httpd.handle_request()
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
print("\n\n✗ Authentication cancelled.")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fix org_info file to add missing founderId field."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
ORG_INFO_PATH = Path.home() / ".cursor" / "aurora_org_info.json"
|
|
9
|
+
|
|
10
|
+
def fix_org_info():
|
|
11
|
+
"""Add founderId to org_info if missing."""
|
|
12
|
+
if not ORG_INFO_PATH.exists():
|
|
13
|
+
print("Error: org_info file does not exist. Please run: aurora-authenticate")
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
with open(ORG_INFO_PATH) as f:
|
|
18
|
+
org_info = json.load(f)
|
|
19
|
+
|
|
20
|
+
print(f"Current org_info: {json.dumps(org_info, indent=2)}")
|
|
21
|
+
|
|
22
|
+
# Add founderId if missing
|
|
23
|
+
if not org_info.get("founderId"):
|
|
24
|
+
founder_id = org_info.get("user_id") or org_info.get("userId")
|
|
25
|
+
if founder_id:
|
|
26
|
+
org_info["founderId"] = founder_id
|
|
27
|
+
with open(ORG_INFO_PATH, 'w') as f:
|
|
28
|
+
json.dump(org_info, f, indent=2)
|
|
29
|
+
print(f"\n✅ Updated org_info with founderId: {founder_id}")
|
|
30
|
+
print(f"Updated org_info: {json.dumps(org_info, indent=2)}")
|
|
31
|
+
return True
|
|
32
|
+
else:
|
|
33
|
+
print("\n❌ Error: No user_id found in org_info. Please re-authenticate with: aurora-authenticate")
|
|
34
|
+
return False
|
|
35
|
+
else:
|
|
36
|
+
print(f"\n✅ org_info already has founderId: {org_info.get('founderId')}")
|
|
37
|
+
return True
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Error fixing org_info: {e}", file=sys.stderr)
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
success = fix_org_info()
|
|
44
|
+
sys.exit(0 if success else 1)
|
finta_aurora_mcp/mcp.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Aurora MCP Proxy Server
|
|
4
|
+
|
|
5
|
+
This creates a local MCP server that proxies requests to the Aurora server
|
|
6
|
+
with authentication. Similar to how Firebase MCP handles auth via CLI login.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from mcp.server import Server
|
|
17
|
+
from mcp.server.stdio import stdio_server
|
|
18
|
+
from mcp.types import Tool, TextContent
|
|
19
|
+
|
|
20
|
+
AURORA_URL = "https://aurora-v1-staging-12f0e746ba305c099eb77fd9f5494283.us.langgraph.app"
|
|
21
|
+
TOKEN_PATH = Path.home() / ".cursor" / "aurora_token.json"
|
|
22
|
+
ORG_INFO_PATH = Path.home() / ".cursor" / "aurora_org_info.json"
|
|
23
|
+
FINTA_API_URL = "https://us-central1-equity-token-stage.cloudfunctions.net"
|
|
24
|
+
|
|
25
|
+
def get_token():
|
|
26
|
+
"""Load stored token."""
|
|
27
|
+
try:
|
|
28
|
+
if TOKEN_PATH.exists():
|
|
29
|
+
with open(TOKEN_PATH) as f:
|
|
30
|
+
data = json.load(f)
|
|
31
|
+
return data.get("access_token")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(f"Error loading token: {e}", file=sys.stderr)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def get_org_info():
|
|
37
|
+
"""Load stored org info."""
|
|
38
|
+
try:
|
|
39
|
+
if ORG_INFO_PATH.exists():
|
|
40
|
+
with open(ORG_INFO_PATH) as f:
|
|
41
|
+
return json.load(f)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Error loading org info: {e}", file=sys.stderr)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
async def fetch_and_store_org_info(token: str):
|
|
47
|
+
"""Fetch org info from the validation endpoint and store it."""
|
|
48
|
+
try:
|
|
49
|
+
async with httpx.AsyncClient() as client:
|
|
50
|
+
response = await client.post(
|
|
51
|
+
f"{FINTA_API_URL}/validateAuroraOAuthToken",
|
|
52
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
53
|
+
timeout=10.0
|
|
54
|
+
)
|
|
55
|
+
if response.status_code == 200:
|
|
56
|
+
data = response.json()
|
|
57
|
+
if data.get("valid"):
|
|
58
|
+
user_id = data.get("userId")
|
|
59
|
+
org_info = {
|
|
60
|
+
"organization_id": data.get("organizationId"),
|
|
61
|
+
"organization_name": data.get("organizationName"),
|
|
62
|
+
"user_id": user_id,
|
|
63
|
+
"founderId": user_id, # Required by Aurora tools (e.g., editInvestorTool)
|
|
64
|
+
"handle": data.get("organizationId"),
|
|
65
|
+
}
|
|
66
|
+
with open(ORG_INFO_PATH, 'w') as f:
|
|
67
|
+
json.dump(org_info, f, indent=2)
|
|
68
|
+
return org_info
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"Error fetching org info: {e}", file=sys.stderr)
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
async def fetch_deal_info(org_info: dict, token: str = None) -> str:
|
|
74
|
+
"""Fetch deal info for the organization (authenticated)."""
|
|
75
|
+
if not org_info:
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
handle = org_info.get("handle") or org_info.get("organization_id")
|
|
79
|
+
if not handle:
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
# Get token if not provided
|
|
83
|
+
if not token:
|
|
84
|
+
token = get_token()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
async with httpx.AsyncClient() as client:
|
|
88
|
+
url = f"{FINTA_API_URL}/fintaAI/tools/get-deal-info"
|
|
89
|
+
headers = {}
|
|
90
|
+
if token:
|
|
91
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
92
|
+
response = await client.get(
|
|
93
|
+
url,
|
|
94
|
+
params={"organizationHandle": handle},
|
|
95
|
+
headers=headers,
|
|
96
|
+
timeout=15.0
|
|
97
|
+
)
|
|
98
|
+
if response.status_code == 200:
|
|
99
|
+
data = response.json()
|
|
100
|
+
return data.get("formatted", "")
|
|
101
|
+
elif response.status_code == 401:
|
|
102
|
+
print(f"Auth error fetching deal info: {response.text}", file=sys.stderr)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Error fetching deal info: {e}", file=sys.stderr)
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
async def fetch_crm_contacts(org_info: dict, token: str = None) -> str:
|
|
108
|
+
"""Fetch CRM contacts for the organization (authenticated)."""
|
|
109
|
+
if not org_info:
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
handle = org_info.get("handle") or org_info.get("organization_id")
|
|
113
|
+
if not handle:
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
# Get token if not provided
|
|
117
|
+
if not token:
|
|
118
|
+
token = get_token()
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
async with httpx.AsyncClient() as client:
|
|
122
|
+
url = f"{FINTA_API_URL}/fintaAI/tools/list-investors"
|
|
123
|
+
headers = {}
|
|
124
|
+
if token:
|
|
125
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
126
|
+
response = await client.get(
|
|
127
|
+
url,
|
|
128
|
+
params={"organizationHandle": handle},
|
|
129
|
+
headers=headers,
|
|
130
|
+
timeout=15.0
|
|
131
|
+
)
|
|
132
|
+
if response.status_code == 200:
|
|
133
|
+
data = response.json()
|
|
134
|
+
return data.get("formatted", "") or data.get("formattedInvestors", "")
|
|
135
|
+
elif response.status_code == 401:
|
|
136
|
+
print(f"Auth error fetching CRM contacts: {response.text}", file=sys.stderr)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Error fetching CRM contacts: {e}", file=sys.stderr)
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
server = Server("aurora-proxy")
|
|
142
|
+
|
|
143
|
+
@server.list_tools()
|
|
144
|
+
async def list_tools():
|
|
145
|
+
"""List available Aurora tools."""
|
|
146
|
+
token = get_token()
|
|
147
|
+
if not token:
|
|
148
|
+
return [
|
|
149
|
+
Tool(
|
|
150
|
+
name="aurora_login",
|
|
151
|
+
description="You need to authenticate first. Run: aurora-authenticate",
|
|
152
|
+
inputSchema={"type": "object", "properties": {}}
|
|
153
|
+
)
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# Always return aurora_chat tool (primary interface)
|
|
157
|
+
# The /mcp endpoint might not exist, so we don't rely on it
|
|
158
|
+
return [
|
|
159
|
+
Tool(
|
|
160
|
+
name="aurora_chat",
|
|
161
|
+
description="Chat with Aurora AI assistant. Aurora has access to tools like searching the web, scraping websites, querying knowledge bases, managing CRM contacts, and more.",
|
|
162
|
+
inputSchema={
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"message": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "Your message to Aurora. Aurora will automatically use tools as needed to answer your question."
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"required": ["message"]
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
@server.call_tool()
|
|
176
|
+
async def call_tool(name: str, arguments: dict):
|
|
177
|
+
"""Handle tool calls."""
|
|
178
|
+
token = get_token()
|
|
179
|
+
if not token:
|
|
180
|
+
return [TextContent(
|
|
181
|
+
type="text",
|
|
182
|
+
text="Not authenticated. Run: aurora-authenticate"
|
|
183
|
+
)]
|
|
184
|
+
|
|
185
|
+
if name == "aurora_chat":
|
|
186
|
+
return await handle_aurora_chat(token, arguments)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
async with httpx.AsyncClient() as client:
|
|
190
|
+
response = await client.post(
|
|
191
|
+
f"{AURORA_URL}/mcp",
|
|
192
|
+
headers={
|
|
193
|
+
"Authorization": f"Bearer {token}",
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
"Accept": "application/json"
|
|
196
|
+
},
|
|
197
|
+
json={
|
|
198
|
+
"jsonrpc": "2.0",
|
|
199
|
+
"method": "tools/call",
|
|
200
|
+
"params": {
|
|
201
|
+
"name": name,
|
|
202
|
+
"arguments": arguments
|
|
203
|
+
},
|
|
204
|
+
"id": 1
|
|
205
|
+
},
|
|
206
|
+
timeout=60.0
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if response.status_code == 200:
|
|
210
|
+
data = response.json()
|
|
211
|
+
if "result" in data and "content" in data["result"]:
|
|
212
|
+
return data["result"]["content"]
|
|
213
|
+
elif "error" in data:
|
|
214
|
+
return [TextContent(
|
|
215
|
+
type="text",
|
|
216
|
+
text=f"Error: {data['error'].get('message', 'Unknown error')}"
|
|
217
|
+
)]
|
|
218
|
+
else:
|
|
219
|
+
return [TextContent(
|
|
220
|
+
type="text",
|
|
221
|
+
text=f"Aurora server error: {response.status_code} - {response.text}"
|
|
222
|
+
)]
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return [TextContent(
|
|
225
|
+
type="text",
|
|
226
|
+
text=f"Error calling Aurora: {str(e)}"
|
|
227
|
+
)]
|
|
228
|
+
|
|
229
|
+
return [TextContent(type="text", text="No response from Aurora")]
|
|
230
|
+
|
|
231
|
+
async def handle_aurora_chat(token: str, arguments: dict):
|
|
232
|
+
"""Handle aurora_chat by calling Aurora's thread API."""
|
|
233
|
+
message = arguments.get("message", "")
|
|
234
|
+
if not message:
|
|
235
|
+
return [TextContent(type="text", text="Error: message is required")]
|
|
236
|
+
|
|
237
|
+
org_info = get_org_info()
|
|
238
|
+
if not org_info:
|
|
239
|
+
org_info = await fetch_and_store_org_info(token)
|
|
240
|
+
|
|
241
|
+
# Ensure org_info has founderId (required by Aurora tools)
|
|
242
|
+
# If missing, try to use user_id as fallback and save it
|
|
243
|
+
if org_info:
|
|
244
|
+
if not org_info.get("founderId"):
|
|
245
|
+
# Try user_id as fallback
|
|
246
|
+
founder_id = org_info.get("user_id") or org_info.get("userId")
|
|
247
|
+
if founder_id:
|
|
248
|
+
org_info["founderId"] = founder_id
|
|
249
|
+
# Save updated org_info back to file
|
|
250
|
+
try:
|
|
251
|
+
with open(ORG_INFO_PATH, 'w') as f:
|
|
252
|
+
json.dump(org_info, f, indent=2)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print(f"Warning: Could not save updated org_info: {e}", file=sys.stderr)
|
|
255
|
+
|
|
256
|
+
# If still missing, try to fetch fresh org_info
|
|
257
|
+
if not org_info.get("founderId"):
|
|
258
|
+
org_info = await fetch_and_store_org_info(token)
|
|
259
|
+
|
|
260
|
+
# Validate that we have the required fields
|
|
261
|
+
if not org_info or not org_info.get("founderId") or not org_info.get("handle"):
|
|
262
|
+
return [TextContent(
|
|
263
|
+
type="text",
|
|
264
|
+
text="Error: Missing required organization information (founderId or handle). Please re-authenticate with: aurora-authenticate"
|
|
265
|
+
)]
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
async with httpx.AsyncClient() as client:
|
|
269
|
+
# Create a fresh thread for each request to avoid state corruption issues
|
|
270
|
+
# This ensures clean state for tool call handling
|
|
271
|
+
response = await client.post(
|
|
272
|
+
f"{AURORA_URL}/threads",
|
|
273
|
+
headers={
|
|
274
|
+
"Authorization": f"Bearer {token}",
|
|
275
|
+
"Content-Type": "application/json"
|
|
276
|
+
},
|
|
277
|
+
json={},
|
|
278
|
+
timeout=10.0
|
|
279
|
+
)
|
|
280
|
+
if response.status_code == 200:
|
|
281
|
+
data = response.json()
|
|
282
|
+
thread_id = data.get("thread_id")
|
|
283
|
+
if not thread_id:
|
|
284
|
+
return [TextContent(
|
|
285
|
+
type="text",
|
|
286
|
+
text="Error: No thread_id returned from Aurora"
|
|
287
|
+
)]
|
|
288
|
+
else:
|
|
289
|
+
return [TextContent(
|
|
290
|
+
type="text",
|
|
291
|
+
text=f"Error creating thread: {response.status_code} - {response.text}"
|
|
292
|
+
)]
|
|
293
|
+
|
|
294
|
+
deal_info_text, crm_contacts = await asyncio.gather(
|
|
295
|
+
fetch_deal_info(org_info, token),
|
|
296
|
+
fetch_crm_contacts(org_info, token)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
deal_info_parts = []
|
|
300
|
+
if deal_info_text:
|
|
301
|
+
deal_info_parts.append(deal_info_text)
|
|
302
|
+
|
|
303
|
+
if crm_contacts:
|
|
304
|
+
crm_section = f"\n=== MY CRM CONTACTS ===\nBelow is a complete list of all investors in my CRM. When asked \"who is in my CRM\", \"list my contacts\", or similar questions, use this data directly to answer.\nColumns: Investor Name; Organization Name; Status; Amount; Time Engaged; Email\n{crm_contacts}\n=== END CRM CONTACTS ==="
|
|
305
|
+
deal_info_parts.append(crm_section)
|
|
306
|
+
|
|
307
|
+
deal_info = "\n".join(deal_info_parts)
|
|
308
|
+
|
|
309
|
+
# Ensure org_info is properly formatted for Aurora tools
|
|
310
|
+
# Double-check founderId is present before sending to Aurora
|
|
311
|
+
if not org_info.get("founderId"):
|
|
312
|
+
# Last resort: try to get it from user_id
|
|
313
|
+
founder_id = org_info.get("user_id") or org_info.get("userId")
|
|
314
|
+
if founder_id:
|
|
315
|
+
org_info["founderId"] = founder_id
|
|
316
|
+
print(f"Added founderId dynamically: {founder_id}", file=sys.stderr)
|
|
317
|
+
else:
|
|
318
|
+
print(f"ERROR: org_info missing founderId and user_id: {org_info}", file=sys.stderr)
|
|
319
|
+
return [TextContent(
|
|
320
|
+
type="text",
|
|
321
|
+
text="Error: Missing founderId in organization info. Please re-authenticate with: aurora-authenticate"
|
|
322
|
+
)]
|
|
323
|
+
|
|
324
|
+
# Debug: Verify org_info structure
|
|
325
|
+
print(f"DEBUG: Sending org_info to Aurora: founderId={org_info.get('founderId')}, handle={org_info.get('handle')}", file=sys.stderr)
|
|
326
|
+
|
|
327
|
+
run_config = {
|
|
328
|
+
"configurable": {
|
|
329
|
+
"model_name": "gpt-5-mini",
|
|
330
|
+
"org_info": org_info, # This must include founderId and handle
|
|
331
|
+
"deal_info": deal_info,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Use the streaming endpoint (matches webapp's approach)
|
|
336
|
+
# This ensures proper tool call handling
|
|
337
|
+
response = await client.post(
|
|
338
|
+
f"{AURORA_URL}/threads/{thread_id}/runs/stream",
|
|
339
|
+
headers={
|
|
340
|
+
"Authorization": f"Bearer {token}",
|
|
341
|
+
"Content-Type": "application/json",
|
|
342
|
+
"Accept": "text/event-stream"
|
|
343
|
+
},
|
|
344
|
+
json={
|
|
345
|
+
"assistant_id": "agent",
|
|
346
|
+
"input": {
|
|
347
|
+
"messages": [{"role": "human", "content": message}]
|
|
348
|
+
},
|
|
349
|
+
"config": run_config,
|
|
350
|
+
"stream_mode": "messages"
|
|
351
|
+
},
|
|
352
|
+
timeout=180.0
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if response.status_code != 200:
|
|
356
|
+
return [TextContent(
|
|
357
|
+
type="text",
|
|
358
|
+
text=f"Aurora server error: {response.status_code} - {response.text}"
|
|
359
|
+
)]
|
|
360
|
+
|
|
361
|
+
last_ai_message = None
|
|
362
|
+
buffer = ""
|
|
363
|
+
stream_complete = False
|
|
364
|
+
|
|
365
|
+
# Process the stream completely - this ensures all tool calls are executed
|
|
366
|
+
try:
|
|
367
|
+
async for chunk in response.aiter_bytes():
|
|
368
|
+
if not chunk:
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
buffer += chunk.decode('utf-8', errors='ignore')
|
|
372
|
+
lines = buffer.split('\n')
|
|
373
|
+
buffer = lines.pop() if lines else ""
|
|
374
|
+
|
|
375
|
+
for line in lines:
|
|
376
|
+
line = line.strip()
|
|
377
|
+
if not line or line.startswith(':'):
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
if line.startswith('data: '):
|
|
381
|
+
data_str = line[6:]
|
|
382
|
+
if data_str == '[DONE]':
|
|
383
|
+
stream_complete = True
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
data = json.loads(data_str)
|
|
388
|
+
|
|
389
|
+
# Handle different stream event types
|
|
390
|
+
if isinstance(data, dict):
|
|
391
|
+
# Check for run status updates
|
|
392
|
+
if data.get("event") == "end":
|
|
393
|
+
stream_complete = True
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
# Handle messages array from stream
|
|
397
|
+
if isinstance(data, list):
|
|
398
|
+
for item in data:
|
|
399
|
+
role = item.get("role") or item.get("type", "")
|
|
400
|
+
|
|
401
|
+
# Skip tool messages and user messages
|
|
402
|
+
if role in ["tool", "user"]:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# Collect AI/assistant messages (only those with actual content)
|
|
406
|
+
if role in ["ai", "assistant"]:
|
|
407
|
+
# Skip messages that only have tool calls (wait for tool execution to complete)
|
|
408
|
+
if (item.get("tool_calls") or item.get("additional_kwargs", {}).get("tool_calls")) and not item.get("content"):
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
content = item.get("content", "")
|
|
412
|
+
|
|
413
|
+
# Extract text content
|
|
414
|
+
if isinstance(content, list):
|
|
415
|
+
text_parts = []
|
|
416
|
+
for part in content:
|
|
417
|
+
if isinstance(part, dict):
|
|
418
|
+
text_parts.append(part.get("text", str(part)))
|
|
419
|
+
else:
|
|
420
|
+
text_parts.append(str(part))
|
|
421
|
+
content = " ".join(text_parts)
|
|
422
|
+
elif isinstance(content, dict):
|
|
423
|
+
content = content.get("text", str(content))
|
|
424
|
+
|
|
425
|
+
if content and isinstance(content, str) and content.strip():
|
|
426
|
+
last_ai_message = content
|
|
427
|
+
except json.JSONDecodeError:
|
|
428
|
+
continue
|
|
429
|
+
except Exception as stream_error:
|
|
430
|
+
# Stream error - fall back to thread state
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
# Wait a moment for any final processing
|
|
434
|
+
if not stream_complete:
|
|
435
|
+
await asyncio.sleep(2)
|
|
436
|
+
|
|
437
|
+
# Always get final state from thread to ensure we have the complete response
|
|
438
|
+
thread_response = await client.get(
|
|
439
|
+
f"{AURORA_URL}/threads/{thread_id}/state",
|
|
440
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
441
|
+
timeout=10.0
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
if thread_response.status_code == 200:
|
|
445
|
+
thread_data = thread_response.json()
|
|
446
|
+
messages = thread_data.get("values", {}).get("messages", [])
|
|
447
|
+
|
|
448
|
+
# Find the last AI message that has actual text content (after all tool calls)
|
|
449
|
+
for msg in reversed(messages):
|
|
450
|
+
role = msg.get("role") or msg.get("type", "")
|
|
451
|
+
if role in ["ai", "assistant"]:
|
|
452
|
+
# Only return messages that have content (not just tool calls)
|
|
453
|
+
# Tool calls should have been executed by now
|
|
454
|
+
if msg.get("tool_calls") and not msg.get("content"):
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
content = msg.get("content", "")
|
|
458
|
+
if isinstance(content, list):
|
|
459
|
+
text_parts = []
|
|
460
|
+
for item in content:
|
|
461
|
+
if isinstance(item, dict):
|
|
462
|
+
text_parts.append(item.get("text", str(item)))
|
|
463
|
+
else:
|
|
464
|
+
text_parts.append(str(item))
|
|
465
|
+
content = " ".join(text_parts)
|
|
466
|
+
elif isinstance(content, dict):
|
|
467
|
+
content = content.get("text", str(content))
|
|
468
|
+
|
|
469
|
+
if content and isinstance(content, str) and content.strip():
|
|
470
|
+
return [TextContent(type="text", text=str(content))]
|
|
471
|
+
|
|
472
|
+
# If we got a message from the stream, use it
|
|
473
|
+
if last_ai_message:
|
|
474
|
+
return [TextContent(type="text", text=last_ai_message)]
|
|
475
|
+
|
|
476
|
+
return [TextContent(
|
|
477
|
+
type="text",
|
|
478
|
+
text="Aurora completed but no text response found. The response may have been tool calls only."
|
|
479
|
+
)]
|
|
480
|
+
except Exception as e:
|
|
481
|
+
return [TextContent(
|
|
482
|
+
type="text",
|
|
483
|
+
text=f"Error calling Aurora: {str(e)}"
|
|
484
|
+
)]
|
|
485
|
+
|
|
486
|
+
return [TextContent(type="text", text="No response from Aurora")]
|
|
487
|
+
|
|
488
|
+
async def main():
|
|
489
|
+
"""Run the MCP server."""
|
|
490
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
491
|
+
await server.run(
|
|
492
|
+
read_stream,
|
|
493
|
+
write_stream,
|
|
494
|
+
server.create_initialization_options()
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
if __name__ == "__main__":
|
|
498
|
+
asyncio.run(main())
|