eulerian-marketing-platform 0.1.1__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,8 +4,12 @@ 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
- from .server import main
8
-
9
- __version__ = "0.1.1"
7
+ __version__ = "0.2.0"
10
8
  __author__ = "Eulerian Technologies"
11
- __all__ = ["main"]
9
+ __all__ = []
10
+
11
+ # Import main only when explicitly requested
12
+ def get_main():
13
+ """Lazy import of main function to avoid side effects."""
14
+ from .server import main
15
+ return main
@@ -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,38 +14,35 @@ 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
- # Configure logging to file and stderr
32
+ # Configure logging to file and stderr with UTF-8 encoding for cross-platform compatibility
33
+ file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8')
34
+ file_handler.setLevel(logging.INFO)
35
+ file_handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
36
+
37
+ # For stderr, use system encoding with error handling
38
+ stream_handler = logging.StreamHandler(sys.stderr)
39
+ stream_handler.setLevel(logging.INFO)
40
+ stream_handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
41
+
42
+ # Configure root logger
41
43
  logging.basicConfig(
42
44
  level=logging.INFO,
43
- format="[%(asctime)s] %(levelname)s: %(message)s",
44
- datefmt="%Y-%m-%d %H:%M:%S",
45
- handlers=[
46
- logging.FileHandler(LOG_FILE),
47
- logging.StreamHandler(sys.stderr)
48
- ]
45
+ handlers=[file_handler, stream_handler]
49
46
  )
50
47
  logger = logging.getLogger(__name__)
51
48
 
@@ -70,235 +67,175 @@ def validate_config() -> None:
70
67
  sys.exit(1)
71
68
 
72
69
 
73
- class EulerianMCPProxy:
74
- """Proxy that forwards MCP requests to remote Eulerian server."""
75
-
76
- def __init__(self):
77
- """Initialize the proxy with configuration."""
78
- self.endpoint = EMP_API_ENDPOINT
79
- self.token = EMP_API_TOKEN
80
- self.timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
81
-
82
- logger.info("=== EULERIAN MCP PROXY START ===")
83
- logger.info(f"Endpoint: {self.endpoint}")
84
- logger.info(f"Token: {self.token[:10] if self.token else 'None'}...")
85
- logger.info(f"Timeout: {self.timeout}s")
86
-
87
- async def forward_request(self, method: str, params: dict = None) -> dict[str, Any]:
88
- """Forward a JSON-RPC request to the remote MCP server.
89
-
90
- Args:
91
- method: The JSON-RPC method name
92
- params: The parameters for the method
93
-
94
- Returns:
95
- The response from the remote server
96
-
97
- Raises:
98
- Exception: If the request fails
99
- """
100
- request_data = {
101
- "jsonrpc": "2.0",
102
- "id": 1,
103
- "method": method,
104
- }
105
- if params:
106
- request_data["params"] = params
107
-
108
- logger.info(f">>> REQUEST: {method}")
109
- logger.debug(f" Full request: {json.dumps(request_data)[:200]}...")
110
-
111
- try:
112
- async with httpx.AsyncClient() as client:
113
- logger.info(f"Forwarding to {self.endpoint}")
114
- response = await client.post(
115
- self.endpoint,
116
- headers={
117
- "Content-Type": "application/json",
118
- "Authorization": f"Bearer {self.token}"
119
- },
120
- json=request_data,
121
- timeout=self.timeout
122
- )
123
-
124
- logger.info(f"<<< RESPONSE: HTTP {response.status_code}")
125
-
126
- if response.status_code != 200:
127
- error_msg = f"HTTP {response.status_code}: {response.reason_phrase}"
128
- logger.error(f" Error: {response.text[:200]}")
129
- raise Exception(error_msg)
130
-
131
- response_data = response.json()
132
- logger.debug(f" Response: {json.dumps(response_data)[:200]}...")
133
-
134
- # Validate JSON-RPC response
135
- if "jsonrpc" not in response_data:
136
- logger.warning(" WARNING: Missing 'jsonrpc' field")
137
-
138
- if "result" in response_data:
139
- logger.info(" Has 'result' field [OK]")
140
- return response_data["result"]
141
- elif "error" in response_data:
142
- logger.error(f" Has 'error' field: {response_data['error']}")
143
- raise Exception(f"Remote error: {response_data['error']}")
144
- else:
145
- logger.warning(" No 'result' or 'error' field")
146
- return response_data
147
-
148
- except httpx.TimeoutException:
149
- logger.error("ERROR: Request timeout")
150
- raise Exception("Request timeout")
151
- except httpx.RequestError as e:
152
- logger.error(f"ERROR: Request failed - {str(e)}")
153
- raise Exception(f"Request failed: {str(e)}")
154
- except json.JSONDecodeError as e:
155
- logger.error(f"ERROR: Invalid JSON response - {str(e)}")
156
- raise Exception(f"Invalid JSON response: {str(e)}")
157
-
158
-
159
- # Create global proxy instance
160
- proxy = EulerianMCPProxy()
161
-
162
- # Create FastMCP server
163
- mcp = FastMCP("eulerian-marketing-platform")
164
-
165
-
166
- # Dynamically fetch and register tools from remote server
167
- @mcp.tool()
168
- async def list_remote_tools() -> dict[str, Any]:
169
- """List all available tools from the remote Eulerian MCP server.
170
-
171
- This tool queries the remote MCP server to discover what tools are available.
172
- Use this to see what operations you can perform on the Eulerian platform.
173
-
174
- Returns:
175
- Dictionary containing the list of available tools with their descriptions
176
- """
177
- try:
178
- result = await proxy.forward_request("tools/list")
179
- return result
180
- except Exception as e:
181
- logger.error(f"Failed to list tools: {str(e)}")
182
- return {"error": str(e), "tools": []}
183
-
184
-
185
- @mcp.tool()
186
- async def call_eulerian_tool(tool_name: str, arguments: dict[str, Any] = None) -> dict[str, Any]:
187
- """Call a tool on the remote Eulerian MCP server.
188
-
189
- This is a generic tool that forwards requests to the remote Eulerian platform.
190
- First use list_remote_tools() to see what tools are available, then call them
191
- using this function.
70
+ def forward_request(request_data: dict) -> dict:
71
+ """Forward a JSON-RPC request to the remote MCP server.
192
72
 
193
73
  Args:
194
- tool_name: The name of the tool to call on the remote server
195
- arguments: Dictionary of arguments to pass to the tool (optional)
74
+ request_data: The JSON-RPC request to forward
196
75
 
197
76
  Returns:
198
- The result from the remote tool execution
199
-
200
- Example:
201
- To call a tool named "search_goal" with no arguments:
202
- >>> call_eulerian_tool("search_goal")
203
-
204
- To call a tool with arguments:
205
- >>> call_eulerian_tool("update_goal", {"action-id": "12345","action-name":"test-mcp"})
77
+ The JSON-RPC response from the remote server
206
78
  """
207
- if arguments is None:
208
- arguments = {}
79
+ timeout = float(os.environ.get("EMP_TIMEOUT", "300"))
209
80
 
210
- try:
211
- params = {
212
- "name": tool_name,
213
- "arguments": arguments
214
- }
215
- result = await proxy.forward_request("tools/call", params)
216
- return result
217
- except Exception as e:
218
- logger.error(f"Failed to call tool '{tool_name}': {str(e)}")
219
- return {"error": str(e), "tool": tool_name}
220
-
221
-
222
- @mcp.tool()
223
- async def get_eulerian_resources() -> dict[str, Any]:
224
- """List all available resources from the remote Eulerian MCP server.
81
+ request_id = request_data.get("id")
82
+ method = request_data.get("method")
225
83
 
226
- Resources are data sources that can be read from the Eulerian platform,
227
- such as configuration files, reports, or reference data.
84
+ logger.info(f">>> REQUEST: {method} (id: {request_id})")
85
+ logger.debug(f" Full request: {json.dumps(request_data)[:200]}...")
228
86
 
229
- Returns:
230
- Dictionary containing the list of available resources
231
- """
232
87
  try:
233
- result = await proxy.forward_request("resources/list")
234
- return result
235
- except Exception as e:
236
- logger.error(f"Failed to list resources: {str(e)}")
237
- return {"error": str(e), "resources": []}
238
-
239
-
240
- @mcp.tool()
241
- async def read_eulerian_resource(uri: str) -> dict[str, Any]:
242
- """Read a specific resource from the remote Eulerian MCP server.
243
-
244
- Args:
245
- uri: The URI of the resource to read (get from get_eulerian_resources())
88
+ logger.info(f"Forwarding to {EMP_API_ENDPOINT}")
246
89
 
247
- Returns:
248
- The content of the requested resource
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
+ )
249
100
 
250
- Example:
251
- >>> read_eulerian_resource("eulerian://config/settings")
252
- """
253
- try:
254
- params = {"uri": uri}
255
- result = await proxy.forward_request("resources/read", params)
256
- return result
257
- except Exception as e:
258
- logger.error(f"Failed to read resource '{uri}': {str(e)}")
259
- return {"error": str(e), "uri": uri}
260
-
261
-
262
- @mcp.tool()
263
- async def get_server_info() -> dict[str, Any]:
264
- """Get information about the remote Eulerian MCP server.
101
+ logger.info(f"<<< RESPONSE: HTTP {response.status_code}")
102
+
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
+
116
+ # Parse response
117
+ try:
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
+
132
+ except json.JSONDecodeError as e:
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
+ }
265
142
 
266
- Returns server capabilities, version, and other metadata.
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
+ }
267
153
 
268
- Returns:
269
- Dictionary containing server information
270
- """
271
- try:
272
- result = await proxy.forward_request("initialize", {
273
- "protocolVersion": "2024-11-05",
274
- "capabilities": {},
275
- "clientInfo": {
276
- "name": "eulerian-mcp-proxy",
277
- "version": "0.1.0"
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)}"
278
162
  }
279
- })
280
- return result
163
+ }
164
+
281
165
  except Exception as e:
282
- logger.error(f"Failed to get server info: {str(e)}")
283
- return {"error": str(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
+ }
284
177
 
285
178
 
286
179
  def main() -> None:
287
- """Entry point for the MCP proxy server."""
288
- # Validate configuration before starting
180
+ """Entry point for the MCP proxy server.
181
+
182
+ Runs a transparent stdio proxy that forwards all JSON-RPC requests
183
+ to the remote Eulerian MCP server.
184
+ """
185
+ # Validate configuration
289
186
  validate_config()
290
187
 
291
- logger.info("Starting Eulerian MCP Proxy Server...")
292
- logger.info("Available tools:")
293
- logger.info(" - list_remote_tools: List all tools from remote server")
294
- logger.info(" - call_eulerian_tool: Call any remote tool")
295
- logger.info(" - get_eulerian_resources: List available resources")
296
- logger.info(" - read_eulerian_resource: Read a specific resource")
297
- 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")
298
193
 
299
- # Run the server in stdio mode (default for MCP)
300
194
  try:
301
- 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
+
302
239
  except KeyboardInterrupt:
303
240
  logger.info("Server stopped by user")
304
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.1
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=Rn4OZQAUsvh-s5Sq2un79qnJ50dnFvAFr0jMHXMVgWQ,291
2
- eulerian_marketing_platform/server.py,sha256=R7qW5Fclv1sbcFEN5L4Upe6c-BwXsod6Q16Tuyxgc9c,10712
3
- eulerian_marketing_platform-0.1.1.dist-info/LICENSE,sha256=eIqBqE_fRsqQJ8F-2v0e-8WzZqdshsCqnzmqLAWrNHU,1078
4
- eulerian_marketing_platform-0.1.1.dist-info/METADATA,sha256=2U6mXMEf2-RecN3bIB_zspSZCgCcoiIJg67zTCa_pZ4,16957
5
- eulerian_marketing_platform-0.1.1.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
6
- eulerian_marketing_platform-0.1.1.dist-info/entry_points.txt,sha256=rrPZptATSS9PUtH9gzCYq0WuP6eahkF-DkdUP1FaYfk,88
7
- eulerian_marketing_platform-0.1.1.dist-info/top_level.txt,sha256=nidh3T6fw-mLjUqZwQ8AiMScS4usuH0WXW4ZgG4HYCo,28
8
- eulerian_marketing_platform-0.1.1.dist-info/RECORD,,