eulerian-marketing-platform 0.1.2__py3-none-any.whl → 0.2.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.
@@ -4,7 +4,7 @@ This package provides a Model Context Protocol (MCP) server that enables
4
4
  AI assistants to interact with Eulerian Marketing Platform APIs.
5
5
  """
6
6
 
7
- __version__ = "0.1.2"
7
+ __version__ = "0.2.0"
8
8
  __author__ = "Eulerian Technologies"
9
9
  __all__ = []
10
10
 
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """Eulerian Marketing Platform MCP Proxy Server.
3
3
 
4
- This server acts as a proxy/bridge between local MCP clients (Claude Desktop, Gemini CLI, etc.)
4
+ This server acts as a transparent proxy between local MCP clients (Claude Desktop, etc.)
5
5
  and a remote Eulerian Marketing Platform MCP server via HTTP.
6
6
 
7
- It uses EMP_API_ENDPOINT and EMP_API_TOKEN environment variables to authenticate
8
- and forward requests to the remote MCP server.
7
+ It forwards ALL MCP requests directly to the remote server, making all remote tools
8
+ and resources available to the client without any intermediary logic.
9
9
  """
10
10
 
11
11
  import os
@@ -14,27 +14,19 @@ import json
14
14
  import logging
15
15
  import tempfile
16
16
  from datetime import datetime
17
- from typing import Any
18
- from pathlib import Path
19
17
  import httpx
20
18
 
21
- from mcp.server.fastmcp import FastMCP
22
- from mcp.server import Server
23
- from mcp.server.stdio import stdio_server
24
- from mcp import types
25
-
26
19
  # Configuration
27
20
  EMP_API_ENDPOINT = os.environ.get("EMP_API_ENDPOINT")
28
21
  EMP_API_TOKEN = os.environ.get("EMP_API_TOKEN")
29
22
 
30
23
  # Logging setup - Use cross-platform temp directory
31
- # Default to system temp directory if EMP_LOG_FILE not set
32
24
  DEFAULT_LOG_FILE = os.path.join(tempfile.gettempdir(), "eulerian-mcp-proxy.log")
33
25
  LOG_FILE = os.environ.get("EMP_LOG_FILE", DEFAULT_LOG_FILE)
34
26
 
35
27
  # Ensure log directory exists
36
28
  log_dir = os.path.dirname(LOG_FILE)
37
- if log_dir: # Only create if there's a directory part
29
+ if log_dir:
38
30
  os.makedirs(log_dir, exist_ok=True)
39
31
 
40
32
  # Configure logging to file and stderr with UTF-8 encoding for cross-platform compatibility
@@ -75,176 +67,175 @@ def validate_config() -> None:
75
67
  sys.exit(1)
76
68
 
77
69
 
78
- class EulerianMCPProxy:
79
- """Proxy that forwards MCP requests to remote Eulerian server."""
70
+ def forward_request(request_data: dict) -> dict:
71
+ """Forward a JSON-RPC request to the remote MCP server.
80
72
 
81
- def __init__(self):
82
- """Initialize the proxy with configuration."""
83
- self.endpoint = EMP_API_ENDPOINT
84
- self.token = EMP_API_TOKEN
85
- self.timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
73
+ Args:
74
+ request_data: The JSON-RPC request to forward
86
75
 
87
- logger.info("=== EULERIAN MCP PROXY START ===")
88
- logger.info(f"Endpoint: {self.endpoint}")
89
- logger.info(f"Token: {self.token[:10] if self.token else 'None'}...")
90
- logger.info(f"Timeout: {self.timeout}s")
76
+ Returns:
77
+ The JSON-RPC response from the remote server
78
+ """
79
+ timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
80
+
81
+ request_id = request_data.get("id")
82
+ method = request_data.get("method")
91
83
 
92
- async def forward_request(self, method: str, params: dict = None) -> dict[str, Any]:
93
- """Forward a JSON-RPC request to the remote MCP server.
84
+ logger.info(f">>> REQUEST: {method} (id: {request_id})")
85
+ logger.debug(f" Full request: {json.dumps(request_data)[:200]}...")
86
+
87
+ try:
88
+ logger.info(f"Forwarding to {EMP_API_ENDPOINT}")
94
89
 
95
- Args:
96
- method: The JSON-RPC method name
97
- params: The parameters for the method
98
-
99
- Returns:
100
- The response from the remote server
101
-
102
- Raises:
103
- Exception: If the request fails
104
- """
105
- request_data = {
106
- "jsonrpc": "2.0",
107
- "id": 1,
108
- "method": method,
109
- }
110
- if params:
111
- request_data["params"] = params
90
+ # Use httpx for sync HTTP request
91
+ with httpx.Client(timeout=timeout) as client:
92
+ response = client.post(
93
+ EMP_API_ENDPOINT,
94
+ headers={
95
+ "Content-Type": "application/json",
96
+ "Authorization": f"Bearer {EMP_API_TOKEN}"
97
+ },
98
+ json=request_data
99
+ )
100
+
101
+ logger.info(f"<<< RESPONSE: HTTP {response.status_code}")
112
102
 
113
- logger.info(f">>> REQUEST: {method}")
114
- logger.debug(f" Full request: {json.dumps(request_data)[:200]}...")
103
+ if response.status_code != 200:
104
+ error_msg = f"HTTP {response.status_code}: {response.reason_phrase}"
105
+ logger.error(f" Error: {response.text[:200]}")
106
+
107
+ return {
108
+ "jsonrpc": "2.0",
109
+ "id": request_id,
110
+ "error": {
111
+ "code": -32000,
112
+ "message": error_msg
113
+ }
114
+ }
115
115
 
116
+ # Parse response
116
117
  try:
117
- async with httpx.AsyncClient() as client:
118
- logger.info(f"Forwarding to {self.endpoint}")
119
- response = await client.post(
120
- self.endpoint,
121
- headers={
122
- "Content-Type": "application/json",
123
- "Authorization": f"Bearer {self.token}"
124
- },
125
- json=request_data,
126
- timeout=self.timeout
127
- )
128
-
129
- logger.info(f"<<< RESPONSE: HTTP {response.status_code}")
130
-
131
- if response.status_code != 200:
132
- error_msg = f"HTTP {response.status_code}: {response.reason_phrase}"
133
- logger.error(f" Error: {response.text[:200]}")
134
- raise Exception(error_msg)
135
-
136
- response_data = response.json()
137
- logger.debug(f" Response: {json.dumps(response_data)[:200]}...")
138
-
139
- # Validate JSON-RPC response
140
- if "jsonrpc" not in response_data:
141
- logger.warning(" WARNING: Missing 'jsonrpc' field")
142
-
143
- if "result" in response_data:
144
- logger.info(" Has 'result' field [OK]")
145
- return response_data["result"]
146
- elif "error" in response_data:
147
- logger.error(f" Has 'error' field: {response_data['error']}")
148
- raise Exception(f"Remote error: {response_data['error']}")
149
- else:
150
- logger.warning(" No 'result' or 'error' field")
151
- return response_data
152
-
153
- except httpx.TimeoutException:
154
- logger.error("ERROR: Request timeout")
155
- raise Exception("Request timeout")
156
- except httpx.RequestError as e:
157
- logger.error(f"ERROR: Request failed - {str(e)}")
158
- raise Exception(f"Request failed: {str(e)}")
118
+ response_data = response.json()
119
+ logger.debug(f" Response: {json.dumps(response_data)[:200]}...")
120
+
121
+ # Validate JSON-RPC response
122
+ if "jsonrpc" not in response_data:
123
+ logger.warning(" WARNING: Missing 'jsonrpc' field")
124
+
125
+ if "result" in response_data:
126
+ logger.info(" Has 'result' field [OK]")
127
+ elif "error" in response_data:
128
+ logger.info(f" Has 'error' field: {response_data['error']}")
129
+
130
+ return response_data
131
+
159
132
  except json.JSONDecodeError as e:
160
- logger.error(f"ERROR: Invalid JSON response - {str(e)}")
161
- raise Exception(f"Invalid JSON response: {str(e)}")
162
-
163
-
164
- # Dynamically fetch and register tools from remote server
165
- # (Tools are registered in main() to avoid running on import)
166
-
167
-
168
- def main() -> None:
169
- """Entry point for the MCP proxy server."""
170
- # Validate configuration before starting
171
- validate_config()
172
-
173
- # Create proxy and server instances (moved here to avoid running on import)
174
- global proxy, mcp
175
- proxy = EulerianMCPProxy()
176
- mcp = FastMCP("eulerian-marketing-platform")
177
-
178
- # Register tools
179
- @mcp.tool()
180
- async def list_remote_tools() -> dict[str, Any]:
181
- """List all available tools from the remote Eulerian MCP server."""
182
- try:
183
- result = await proxy.forward_request("tools/list")
184
- return result
185
- except Exception as e:
186
- logger.error(f"Failed to list tools: {str(e)}")
187
- return {"error": str(e), "tools": []}
133
+ logger.error(f" ERROR: Invalid JSON - {e}")
134
+ return {
135
+ "jsonrpc": "2.0",
136
+ "id": request_id,
137
+ "error": {
138
+ "code": -32700,
139
+ "message": f"Invalid JSON: {str(e)}"
140
+ }
141
+ }
188
142
 
189
- @mcp.tool()
190
- async def call_eulerian_tool(tool_name: str, arguments: dict[str, Any] = None) -> dict[str, Any]:
191
- """Call a tool on the remote Eulerian MCP server."""
192
- if arguments is None:
193
- arguments = {}
194
- try:
195
- params = {"name": tool_name, "arguments": arguments}
196
- result = await proxy.forward_request("tools/call", params)
197
- return result
198
- except Exception as e:
199
- logger.error(f"Failed to call tool '{tool_name}': {str(e)}")
200
- return {"error": str(e), "tool": tool_name}
143
+ except httpx.TimeoutException:
144
+ logger.error("ERROR: Request timeout")
145
+ return {
146
+ "jsonrpc": "2.0",
147
+ "id": request_id,
148
+ "error": {
149
+ "code": -32000,
150
+ "message": "Request timeout"
151
+ }
152
+ }
201
153
 
202
- @mcp.tool()
203
- async def get_eulerian_resources() -> dict[str, Any]:
204
- """List all available resources from the remote Eulerian MCP server."""
205
- try:
206
- result = await proxy.forward_request("resources/list")
207
- return result
208
- except Exception as e:
209
- logger.error(f"Failed to list resources: {str(e)}")
210
- return {"error": str(e), "resources": []}
154
+ except httpx.RequestError as e:
155
+ logger.error(f"ERROR: Request failed - {str(e)}")
156
+ return {
157
+ "jsonrpc": "2.0",
158
+ "id": request_id,
159
+ "error": {
160
+ "code": -32000,
161
+ "message": f"Request failed: {str(e)}"
162
+ }
163
+ }
211
164
 
212
- @mcp.tool()
213
- async def read_eulerian_resource(uri: str) -> dict[str, Any]:
214
- """Read a specific resource from the remote Eulerian MCP server."""
215
- try:
216
- params = {"uri": uri}
217
- result = await proxy.forward_request("resources/read", params)
218
- return result
219
- except Exception as e:
220
- logger.error(f"Failed to read resource '{uri}': {str(e)}")
221
- return {"error": str(e), "uri": uri}
165
+ except Exception as e:
166
+ logger.error(f"ERROR: Unexpected error - {str(e)}")
167
+ import traceback
168
+ logger.error(f"Traceback: {traceback.format_exc()}")
169
+ return {
170
+ "jsonrpc": "2.0",
171
+ "id": request_id,
172
+ "error": {
173
+ "code": -32000,
174
+ "message": f"Error: {str(e)}"
175
+ }
176
+ }
177
+
178
+
179
+ def main() -> None:
180
+ """Entry point for the MCP proxy server.
222
181
 
223
- @mcp.tool()
224
- async def get_server_info() -> dict[str, Any]:
225
- """Get information about the remote Eulerian MCP server."""
226
- try:
227
- result = await proxy.forward_request("initialize", {
228
- "protocolVersion": "2024-11-05",
229
- "capabilities": {},
230
- "clientInfo": {"name": "eulerian-mcp-proxy", "version": "0.1.1"}
231
- })
232
- return result
233
- except Exception as e:
234
- logger.error(f"Failed to get server info: {str(e)}")
235
- return {"error": str(e)}
182
+ Runs a transparent stdio proxy that forwards all JSON-RPC requests
183
+ to the remote Eulerian MCP server.
184
+ """
185
+ # Validate configuration
186
+ validate_config()
236
187
 
237
- logger.info("Starting Eulerian MCP Proxy Server...")
238
- logger.info("Available tools:")
239
- logger.info(" - list_remote_tools: List all tools from remote server")
240
- logger.info(" - call_eulerian_tool: Call any remote tool")
241
- logger.info(" - get_eulerian_resources: List available resources")
242
- logger.info(" - read_eulerian_resource: Read a specific resource")
243
- logger.info(" - get_server_info: Get remote server information")
188
+ logger.info("=== EULERIAN MCP PROXY START ===")
189
+ logger.info(f"Endpoint: {EMP_API_ENDPOINT}")
190
+ logger.info(f"Token: {EMP_API_TOKEN[:10] if EMP_API_TOKEN else 'None'}...")
191
+ logger.info(f"Timeout: {float(os.environ.get('EMP_TIMEOUT', '300'))}s")
192
+ logger.info("Starting stdio proxy - all remote tools will be available")
244
193
 
245
- # Run the server in stdio mode (default for MCP)
246
194
  try:
247
- mcp.run()
195
+ # Read from stdin line by line
196
+ for line in sys.stdin:
197
+ line = line.strip()
198
+ if not line:
199
+ continue
200
+
201
+ try:
202
+ # Parse request
203
+ request_data = json.loads(line)
204
+
205
+ # Forward to remote server
206
+ response_data = forward_request(request_data)
207
+
208
+ # Send response to stdout
209
+ response_json = json.dumps(response_data)
210
+ print(response_json, flush=True)
211
+ logger.info(" Response forwarded [OK]")
212
+
213
+ except json.JSONDecodeError as e:
214
+ logger.error(f"ERROR: Invalid JSON in request - {e}")
215
+ error_response = {
216
+ "jsonrpc": "2.0",
217
+ "id": None,
218
+ "error": {
219
+ "code": -32700,
220
+ "message": f"Parse error: {str(e)}"
221
+ }
222
+ }
223
+ print(json.dumps(error_response), flush=True)
224
+
225
+ except Exception as e:
226
+ logger.error(f"ERROR: Unexpected error processing request - {str(e)}")
227
+ import traceback
228
+ logger.error(f"Traceback: {traceback.format_exc()}")
229
+ error_response = {
230
+ "jsonrpc": "2.0",
231
+ "id": None,
232
+ "error": {
233
+ "code": -32000,
234
+ "message": f"Error: {str(e)}"
235
+ }
236
+ }
237
+ print(json.dumps(error_response), flush=True)
238
+
248
239
  except KeyboardInterrupt:
249
240
  logger.info("Server stopped by user")
250
241
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eulerian-marketing-platform
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: MCP server for Eulerian Marketing Platform - enables AI assistants to interact with Eulerian's marketing analytics and campaign management APIs
5
5
  Author-email: Eulerian Technologies <mathieu@eulerian.com>
6
6
  License: MIT
@@ -24,7 +24,6 @@ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: httpx (>=0.25.0)
27
- Requires-Dist: mcp (>=1.2.0)
28
27
  Provides-Extra: deployment
29
28
  Requires-Dist: uv (>=0.5.0) ; extra == 'deployment'
30
29
  Provides-Extra: dev
@@ -0,0 +1,8 @@
1
+ eulerian_marketing_platform/__init__.py,sha256=8Od_2veelx75AfbIEFrCS1UF6AOsKaX6CF4zR_z8yYU,428
2
+ eulerian_marketing_platform/server.py,sha256=y1vy-LvSjG18dvDv65hO875s_7vogbb5bdNwTetKKcU,8335
3
+ eulerian_marketing_platform-0.2.0.dist-info/LICENSE,sha256=eIqBqE_fRsqQJ8F-2v0e-8WzZqdshsCqnzmqLAWrNHU,1078
4
+ eulerian_marketing_platform-0.2.0.dist-info/METADATA,sha256=B0l6BxIaS-m55cFeicvSsHEhwVKZAmwy2OaFM9eWYpY,16928
5
+ eulerian_marketing_platform-0.2.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
6
+ eulerian_marketing_platform-0.2.0.dist-info/entry_points.txt,sha256=rrPZptATSS9PUtH9gzCYq0WuP6eahkF-DkdUP1FaYfk,88
7
+ eulerian_marketing_platform-0.2.0.dist-info/top_level.txt,sha256=nidh3T6fw-mLjUqZwQ8AiMScS4usuH0WXW4ZgG4HYCo,28
8
+ eulerian_marketing_platform-0.2.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- eulerian_marketing_platform/__init__.py,sha256=osuJ8iP6O20ElZXPoZlOQ7sNbZ93kgMz3we8mm_B2-Y,428
2
- eulerian_marketing_platform/server.py,sha256=bUVn_b05ZritTtNflldCxoFn8YwyrQBqrcbQZ5ceZAg,9810
3
- eulerian_marketing_platform-0.1.2.dist-info/LICENSE,sha256=eIqBqE_fRsqQJ8F-2v0e-8WzZqdshsCqnzmqLAWrNHU,1078
4
- eulerian_marketing_platform-0.1.2.dist-info/METADATA,sha256=gfE3v09r1B4dd8Vi_snjgd9JQDWaHplKnZMsp1ML-34,16957
5
- eulerian_marketing_platform-0.1.2.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
6
- eulerian_marketing_platform-0.1.2.dist-info/entry_points.txt,sha256=rrPZptATSS9PUtH9gzCYq0WuP6eahkF-DkdUP1FaYfk,88
7
- eulerian_marketing_platform-0.1.2.dist-info/top_level.txt,sha256=nidh3T6fw-mLjUqZwQ8AiMScS4usuH0WXW4ZgG4HYCo,28
8
- eulerian_marketing_platform-0.1.2.dist-info/RECORD,,