|
| 1 | +""" |
| 2 | +Flask-based MCP server exposing a single `python_session` tool. |
| 3 | +
|
| 4 | +Install deps: |
| 5 | +
|
| 6 | + pip install mcp-utils flask |
| 7 | +
|
| 8 | +Usage: |
| 9 | +
|
| 10 | + python examples/python_session_flask.py |
| 11 | +
|
| 12 | +Notes: |
| 13 | +- Keeps a persistent Python execution context across calls (shared state). |
| 14 | +- Returns captured stdout/stderr and the value of the final expression, if any. |
| 15 | +""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +import ast |
| 20 | +import io |
| 21 | +import logging |
| 22 | +import sys |
| 23 | +from contextlib import redirect_stdout, redirect_stderr |
| 24 | + |
| 25 | +from flask import Flask, jsonify, request |
| 26 | +import msgspec |
| 27 | + |
| 28 | +from mcp_utils.core import MCPServer |
| 29 | +from mcp_utils.queue import SQLiteResponseQueue |
| 30 | +from mcp_utils.schema import CallToolResult, TextContent |
| 31 | + |
| 32 | + |
| 33 | +app = Flask(__name__) |
| 34 | +mcp = MCPServer("python-session", "1.0", response_queue=SQLiteResponseQueue("responses.db")) |
| 35 | + |
| 36 | +logger = logging.getLogger("mcp_utils") |
| 37 | +logger.setLevel(logging.DEBUG) |
| 38 | + |
| 39 | + |
| 40 | +# A single, persistent execution context shared across requests |
| 41 | +_EXEC_CONTEXT: dict[str, object] = {} |
| 42 | + |
| 43 | + |
| 44 | +def _run_code_in_persistent_context(code: str, context: dict[str, object]) -> tuple[str, bool]: |
| 45 | + """Execute code in a persistent context and return (output, is_error). |
| 46 | +
|
| 47 | + - Captures stdout and stderr |
| 48 | + - If the last statement is an expression, evaluates and appends its repr |
| 49 | + """ |
| 50 | + stdout = io.StringIO() |
| 51 | + result_text = "" |
| 52 | + is_error = False |
| 53 | + |
| 54 | + try: |
| 55 | + module = ast.parse(code, mode="exec") |
| 56 | + body = module.body |
| 57 | + with redirect_stdout(stdout), redirect_stderr(stdout): |
| 58 | + if body and isinstance(body[-1], ast.Expr): |
| 59 | + # Execute all but last, then eval last expression and show its repr |
| 60 | + exec(compile(ast.Module(body=body[:-1], type_ignores=[]), "<session>", "exec"), context, context) |
| 61 | + last_expr = ast.Expression(body[-1].value) |
| 62 | + value = eval(compile(last_expr, "<session>", "eval"), context, context) |
| 63 | + if value is not None: |
| 64 | + print(repr(value)) |
| 65 | + else: |
| 66 | + exec(compile(module, "<session>", "exec"), context, context) |
| 67 | + except Exception as e: # noqa: BLE001 - return error text to client |
| 68 | + is_error = True |
| 69 | + # Include exception type and message; details in stdout if any |
| 70 | + result_text = f"{e.__class__.__name__}: {e}" |
| 71 | + |
| 72 | + output = stdout.getvalue() |
| 73 | + if result_text: |
| 74 | + output = (output + ("\n" if output and not output.endswith("\n") else "") + result_text).rstrip() |
| 75 | + return (output if output else ("" if not is_error else result_text)), is_error |
| 76 | + |
| 77 | + |
| 78 | +@mcp.tool() |
| 79 | +def execute_code(code: str) -> CallToolResult: |
| 80 | + """Execute Python code in a session. |
| 81 | +
|
| 82 | + Code executes in a persistent, shared context across calls. |
| 83 | + """ |
| 84 | + output, is_error = _run_code_in_persistent_context(code, _EXEC_CONTEXT) |
| 85 | + # Always return a string content payload with error flag as appropriate |
| 86 | + return CallToolResult(content=[TextContent(text=output or "")], is_error=is_error) |
| 87 | + |
| 88 | + |
| 89 | +@app.route("/mcp", methods=["POST"]) |
| 90 | +def mcp_route(): |
| 91 | + response = mcp.handle_message(request.get_json()) |
| 92 | + return jsonify(msgspec.to_builtins(response)) |
| 93 | + |
| 94 | + |
| 95 | +if __name__ == "__main__": |
| 96 | + handler = logging.StreamHandler(sys.stdout) |
| 97 | + formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(name)s: %(message)s") |
| 98 | + handler.setFormatter(formatter) |
| 99 | + logger.addHandler(handler) |
| 100 | + # Run with Flask's built-in dev server locally |
| 101 | + app.run(host="127.0.0.1", port=9005, debug=True) |
0 commit comments