ghh1125 commited on
Commit
bec983e
·
verified ·
1 Parent(s): 254f4ec

Upload 14 files

Browse files
Dockerfile CHANGED
@@ -1,18 +1,23 @@
1
- FROM python:3.10
2
 
3
- RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
- USER user
5
- ENV PATH="/home/user/.local/bin:$PATH"
 
6
 
7
  WORKDIR /app
8
 
9
- COPY --chown=user ./requirements.txt requirements.txt
10
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
11
 
12
- COPY --chown=user . /app
13
  ENV MCP_TRANSPORT=http
14
  ENV MCP_PORT=7860
15
 
16
  EXPOSE 7860
17
 
18
- CMD ["python", "snap-python/mcp_output/start_mcp.py"]
 
 
 
1
+ FROM python:3.11-slim
2
 
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ RUN useradd -m -u 1000 appuser
7
 
8
  WORKDIR /app
9
 
10
+ COPY requirements.txt /app/requirements.txt
11
+ RUN pip install --no-cache-dir -r /app/requirements.txt
12
+
13
+ COPY snap-python/ /app/snap-python/
14
+ COPY app.py /app/app.py
15
 
 
16
  ENV MCP_TRANSPORT=http
17
  ENV MCP_PORT=7860
18
 
19
  EXPOSE 7860
20
 
21
+ USER appuser
22
+
23
+ CMD ["python", "snap-python/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,74 @@
1
  ---
2
- title: Snap Python
3
- emoji: 🌍
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: snap-python MCP Service
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # snap-python MCP Service
12
+
13
+ This deployment package wraps SNAP (Stanford Network Analysis Platform) Python functionality into a FastMCP service.
14
+
15
+ ## Available Tools
16
+
17
+ - `adapter_health`
18
+ - `list_modules`
19
+ - `generate_graph`
20
+ - `graph_info`
21
+ - `basic_metrics`
22
+ - `connectivity_metrics`
23
+ - `degree_distribution`
24
+ - `call_snap_function`
25
+
26
+ Detailed parameter documentation and examples are in `snap-python/mcp_output/README_MCP.md`.
27
+
28
+ ## Local stdio usage (Claude Desktop / CLI)
29
+
30
+ ```bash
31
+ cd snap-python/mcp_output
32
+ python start_mcp.py
33
+ ```
34
+
35
+ or:
36
+
37
+ ```bash
38
+ cd snap-python/mcp_output/mcp_plugin
39
+ python main.py
40
+ ```
41
+
42
+ Default transport is `stdio`.
43
+
44
+ ## HTTP client usage
45
+
46
+ ```bash
47
+ cd snap-python/mcp_output
48
+ MCP_TRANSPORT=http MCP_PORT=8000 python start_mcp.py
49
+ ```
50
+
51
+ Clients connect to:
52
+
53
+ ```text
54
+ http://localhost:8000/mcp
55
+ ```
56
+
57
+ ## Docker / HuggingFace Spaces
58
+
59
+ Docker entry point runs FastMCP directly:
60
+
61
+ ```text
62
+ python snap-python/mcp_output/start_mcp.py
63
+ ```
64
+
65
+ With environment:
66
+
67
+ - `MCP_TRANSPORT=http`
68
+ - `MCP_PORT=7860`
69
+
70
+ Client endpoint:
71
+
72
+ ```text
73
+ https://<your-space-host>/mcp
74
+ ```
app.py CHANGED
@@ -1,45 +1,58 @@
1
- from fastapi import FastAPI
 
2
  import os
3
  import sys
 
 
 
 
4
 
5
- mcp_plugin_path = os.path.join(os.path.dirname(__file__), "snap-python", "mcp_output", "mcp_plugin")
6
- sys.path.insert(0, mcp_plugin_path)
7
 
8
- app = FastAPI(
9
- title="Snap-Python MCP Service",
10
- description="Auto-generated MCP service for snap-python",
11
- version="1.0.0"
12
- )
 
 
13
 
14
  @app.get("/")
15
- def root():
16
  return {
17
- "service": "Snap-Python MCP Service",
18
- "version": "1.0.0",
19
- "status": "running",
20
- "transport": os.environ.get("MCP_TRANSPORT", "http")
21
  }
22
 
 
23
  @app.get("/health")
24
- def health_check():
25
- return {"status": "healthy", "service": "snap-python MCP"}
 
26
 
27
  @app.get("/tools")
28
- def list_tools():
29
- try:
30
- from mcp_service import create_app
31
- mcp_app = create_app()
32
- tools = []
33
- for tool_name, tool_func in mcp_app.tools.items():
34
- tools.append({
35
- "name": tool_name,
36
- "description": tool_func.__doc__ or "No description available"
37
- })
38
- return {"tools": tools}
39
- except Exception as e:
40
- return {"error": f"Failed to load tools: {str(e)}"}
 
 
 
 
 
 
41
 
42
  if __name__ == "__main__":
43
  import uvicorn
44
- port = int(os.environ.get("PORT", 7860))
45
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ from __future__ import annotations
2
+
3
  import os
4
  import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from fastapi import FastAPI
9
 
 
 
10
 
11
+ BASE_DIR = Path(__file__).resolve().parent
12
+ PLUGIN_DIR = BASE_DIR / "snap-python" / "mcp_output" / "mcp_plugin"
13
+ if str(PLUGIN_DIR) not in sys.path:
14
+ sys.path.insert(0, str(PLUGIN_DIR))
15
+
16
+ app = FastAPI(title="snap-python MCP Info API", version="1.0.0")
17
+
18
 
19
  @app.get("/")
20
+ def root() -> dict[str, Any]:
21
  return {
22
+ "service": "snap-python MCP Service",
23
+ "mcp_transport": os.getenv("MCP_TRANSPORT", "stdio"),
24
+ "mcp_port": int(os.getenv("MCP_PORT", "8000")),
25
+ "info": "This FastAPI app is supplementary and does not run the MCP server.",
26
  }
27
 
28
+
29
  @app.get("/health")
30
+ def health() -> dict[str, str]:
31
+ return {"status": "healthy"}
32
+
33
 
34
  @app.get("/tools")
35
+ def tools() -> dict[str, Any]:
36
+ from mcp_service import create_app
37
+
38
+ mcp = create_app()
39
+ tool_items: list[dict[str, Any]] = []
40
+
41
+ raw_tools = getattr(mcp, "tools", None)
42
+ if isinstance(raw_tools, dict):
43
+ for name, value in raw_tools.items():
44
+ description = getattr(value, "description", "")
45
+ tool_items.append({"name": str(name), "description": str(description)})
46
+ elif isinstance(raw_tools, list):
47
+ for value in raw_tools:
48
+ name = getattr(value, "name", "")
49
+ description = getattr(value, "description", "")
50
+ tool_items.append({"name": str(name), "description": str(description)})
51
+
52
+ return {"count": len(tool_items), "tools": tool_items}
53
+
54
 
55
  if __name__ == "__main__":
56
  import uvicorn
57
+
58
+ uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")), reload=False)
port.json CHANGED
@@ -1,5 +1 @@
1
- {
2
- "repo": "snap-python",
3
- "port": 7916,
4
- "timestamp": 1773243382
5
- }
 
1
+ {"port": 7860}
 
 
 
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
  fastmcp
 
2
  fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
 
1
  fastmcp
2
+ snap-stanford
3
  fastapi
4
+ uvicorn
 
run_docker.ps1 CHANGED
@@ -1,26 +1,8 @@
1
- cd $PSScriptRoot
2
  $ErrorActionPreference = "Stop"
3
- $entryName = if ($env:MCP_ENTRY_NAME) { $env:MCP_ENTRY_NAME } else { "snap-python" }
4
- $entryUrl = if ($env:MCP_ENTRY_URL) { $env:MCP_ENTRY_URL } else { "http://localhost:7916/mcp" }
5
- $imageName = if ($env:MCP_IMAGE_NAME) { $env:MCP_IMAGE_NAME } else { "snap-python-mcp" }
6
- $mcpDir = Join-Path $env:USERPROFILE ".cursor"
7
- $mcpPath = Join-Path $mcpDir "mcp.json"
8
- if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
9
- $config = @{}
10
- if (Test-Path $mcpPath) {
11
- try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
12
- }
13
- $serversOrdered = [ordered]@{}
14
- if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
15
- $existing = $config.mcpServers
16
- if ($existing -is [pscustomobject]) {
17
- foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
18
- } elseif ($existing -is [System.Collections.IDictionary]) {
19
- foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
20
- }
21
- }
22
- $serversOrdered[$entryName] = @{ url = $entryUrl }
23
- $config = @{ mcpServers = $serversOrdered }
24
- $config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
25
  docker build -t $imageName .
26
- docker run --rm -p 7916:7860 $imageName
 
 
1
  $ErrorActionPreference = "Stop"
2
+
3
+ $portConfig = Get-Content -Raw -Path "port.json" | ConvertFrom-Json
4
+ $port = [int]$portConfig.port
5
+ $imageName = "snap-python-mcp"
6
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  docker build -t $imageName .
8
+ docker run --rm -p "${port}:${port}" -e MCP_PORT=$port $imageName
run_docker.sh CHANGED
@@ -1,75 +1,8 @@
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
- cd "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
- mcp_entry_name="${MCP_ENTRY_NAME:-snap-python}"
5
- mcp_entry_url="${MCP_ENTRY_URL:-http://localhost:7916/mcp}"
6
- mcp_dir="${HOME}/.cursor"
7
- mcp_path="${mcp_dir}/mcp.json"
8
- mkdir -p "${mcp_dir}"
9
- if command -v python3 >/dev/null 2>&1; then
10
- python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
11
- import json, os, sys
12
- path, name, url = sys.argv[1:4]
13
- cfg = {"mcpServers": {}}
14
- if os.path.exists(path):
15
- try:
16
- with open(path, "r", encoding="utf-8") as f:
17
- cfg = json.load(f)
18
- except Exception:
19
- cfg = {"mcpServers": {}}
20
- if not isinstance(cfg, dict):
21
- cfg = {"mcpServers": {}}
22
- servers = cfg.get("mcpServers")
23
- if not isinstance(servers, dict):
24
- servers = {}
25
- ordered = {}
26
- for k, v in servers.items():
27
- if k != name:
28
- ordered[k] = v
29
- ordered[name] = {"url": url}
30
- cfg = {"mcpServers": ordered}
31
- with open(path, "w", encoding="utf-8") as f:
32
- json.dump(cfg, f, indent=2, ensure_ascii=False)
33
- PY
34
- elif command -v python >/dev/null 2>&1; then
35
- python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
36
- import json, os, sys
37
- path, name, url = sys.argv[1:4]
38
- cfg = {"mcpServers": {}}
39
- if os.path.exists(path):
40
- try:
41
- with open(path, "r", encoding="utf-8") as f:
42
- cfg = json.load(f)
43
- except Exception:
44
- cfg = {"mcpServers": {}}
45
- if not isinstance(cfg, dict):
46
- cfg = {"mcpServers": {}}
47
- servers = cfg.get("mcpServers")
48
- if not isinstance(servers, dict):
49
- servers = {}
50
- ordered = {}
51
- for k, v in servers.items():
52
- if k != name:
53
- ordered[k] = v
54
- ordered[name] = {"url": url}
55
- cfg = {"mcpServers": ordered}
56
- with open(path, "w", encoding="utf-8") as f:
57
- json.dump(cfg, f, indent=2, ensure_ascii=False)
58
- PY
59
- elif command -v jq >/dev/null 2>&1; then
60
- name="${mcp_entry_name}"; url="${mcp_entry_url}"
61
- if [ -f "${mcp_path}" ]; then
62
- tmp="$(mktemp)"
63
- jq --arg name "$name" --arg url "$url" '
64
- .mcpServers = (.mcpServers // {})
65
- | .mcpServers as $s
66
- | ($s | with_entries(select(.key != $name))) as $base
67
- | .mcpServers = ($base + {($name): {"url": $url}})
68
- ' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
69
- else
70
- printf '{ "mcpServers": { "%s": { "url": "%s" } } }
71
- ' "$name" "$url" > "${mcp_path}"
72
- fi
73
- fi
74
- docker build -t snap-python-mcp .
75
- docker run --rm -p 7916:7860 snap-python-mcp
 
1
  #!/usr/bin/env bash
2
  set -euo pipefail
3
+
4
+ PORT=$(python3 -c "import json;print(json.load(open('port.json'))['port'])")
5
+ IMAGE_NAME="snap-python-mcp"
6
+
7
+ docker build -t "${IMAGE_NAME}" .
8
+ docker run --rm -p "${PORT}:${PORT}" -e MCP_PORT="${PORT}" "${IMAGE_NAME}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
snap-python/mcp_output/README_MCP.md CHANGED
@@ -1,110 +1,115 @@
1
- # snap-python MCP (Model Context Protocol) Service README
2
-
3
- ## 1) Project Introduction
4
-
5
- This MCP (Model Context Protocol) service exposes graph analytics capabilities built on top of the `snap-python` repository (Stanford SNAP bindings + `snapx` Pythonic API).
6
- It is designed for developer workflows where an LLM or client app needs to:
7
-
8
- - Create/load graphs
9
- - Run shortest path and connectivity analysis
10
- - Compute community/centrality metrics
11
- - Convert between Python data structures and graph objects
12
-
13
- Main usable layers:
14
-
15
- - `swig.snap` (high-performance SNAP bindings, broad algorithm coverage)
16
- - `snapx` (NetworkX-like API wrapping SNAP-style behavior)
17
-
18
- ---
19
-
20
- ## 2) Installation Method
21
-
22
- ### Requirements
23
-
24
- - Python 3.x
25
- - `pip`
26
- - Build toolchain for native extension (C/C++ compiler, SWIG-compatible environment) for `swig.snap` usage
27
- - Optional: `networkx`, `numpy`, `scipy` for interoperability or specific algorithm paths
28
-
29
- ### Install
30
-
31
- - Install from repository root:
32
- - `pip install .`
33
- - Editable/dev install:
34
- - `pip install -e .`
35
-
36
- If native build fails, you can still use pure Python `snapx` portions where applicable, but full SNAP performance/features require successful extension build.
37
-
38
- ---
39
-
40
- ## 3) Quick Start
41
-
42
- ### Basic graph + shortest path (snapx-style)
43
-
44
- - Create graph with `snapx.classes.graph.Graph` or generators (`snapx.generators.classic.path_graph`)
45
- - Use unweighted/weighted shortest path modules:
46
- - `snapx.algorithms.shortest_paths.unweighted.single_source_shortest_path`
47
- - `snapx.algorithms.shortest_paths.weighted.dijkstra_path`
48
-
49
- ### Core analysis functions (SNAP SWIG layer)
50
-
51
- Typical high-value functions available in `snap` include:
52
-
53
- - Connectivity: `GetWccs`, `GetSccs`, `IsConnected`, `GetMxWcc`
54
- - Path/BFS: `GetBfsTree`, `GetShortPath`, `GetBfsEffDiam`
55
- - Centrality: `GetDegreeCentr`, `GetClosenessCentr`, `GetBetweennessCentr`
56
- - Community: `CommunityCNM`, `CommunityGirvanNewman`
57
- - Generation: `GenRndGnm`, `GenPrefAttach`, `GenSmallWorld`, `GenRMat`
58
-
59
- ---
60
-
61
- ## 4) Available Tools and Endpoints List
62
-
63
- Recommended MCP (Model Context Protocol) service surface (developer-oriented grouping):
64
-
65
- - `graph.create`
66
- - Create empty/path/basic graphs (snapx generators, SNAP constructors)
67
- - `graph.load`
68
- - Load edge lists / persisted graphs (`LoadEdgeList`, `*_Load`)
69
- - `graph.convert`
70
- - Convert dict-of-dicts and Python structures (`to_snapx_graph`, `from_dict_of_dicts`)
71
- - `analysis.connectivity`
72
- - Components and connectedness (`connected_components`, `GetWccs`, `GetSccs`)
73
- - `analysis.shortest_path`
74
- - Unweighted + Dijkstra + all-pairs (`single_source_shortest_path`, `dijkstra_path`, `johnson`)
75
- - `analysis.centrality`
76
- - Degree/closeness/betweenness/eigenvector variants
77
- - `analysis.community`
78
- - CNM and Girvan-Newman community detection
79
- - `analysis.stats`
80
- - Degree distributions, triads, clustering, diameter/ANF estimates
81
- - `graph.relabel`
82
- - Node relabel operations (`relabel_nodes`, integer relabeling)
83
-
84
- Note: Repository does not define a ready-made CLI endpoint set; expose these as MCP (Model Context Protocol) service methods in your host runtime.
85
-
86
- ---
87
-
88
- ## 5) Common Issues and Notes
89
-
90
- - Native build friction:
91
- - Most installation issues come from missing compiler/SWIG toolchain.
92
- - Mixed API layers:
93
- - `swig.snap` and `snapx` overlap conceptually; keep service contracts explicit about which backend each endpoint uses.
94
- - Performance:
95
- - Prefer SWIG SNAP functions for large graphs and heavy analytics.
96
- - Compatibility:
97
- - Some conversion paths may assume NetworkX-like inputs.
98
- - Testing/examples:
99
- - Use `test/` and `examples/` as validation references for endpoint behavior and expected outputs.
100
-
101
- ---
102
-
103
- ## 6) Reference Links / Documentation
104
-
105
- - Repository: https://github.com/snap-stanford/snap-python
106
- - Root docs: `README.md`, `troubleshooting.md`, `RELEASE.txt`
107
- - SWIG notes: `swig/README.txt`
108
- - Docker/environment help: `docker/README.md`
109
- - Examples: `examples/`, `dev/examples/`
110
- - Tests: `test/`, `dev/test/`
 
 
 
 
 
 
1
+ # snap-python MCP Layer
2
+
3
+ This folder contains the FastMCP adapter layer for exposing core SNAP capabilities as MCP tools.
4
+
5
+ ## Exposed MCP Tools
6
+
7
+ 1. **adapter_health**
8
+ - Parameters: none
9
+ - Returns adapter mode, module load counts, and source path diagnostics.
10
+
11
+ 2. **list_modules**
12
+ - Parameters: none
13
+ - Returns loaded modules and failed imports.
14
+
15
+ 3. **generate_graph**
16
+ - Parameters:
17
+ - `model: str` (`rnd_undirected`, `rnd_directed`, `pref_attach`, `small_world`, `rmat`, `forest_fire`)
18
+ - `num_nodes: int`
19
+ - `num_edges: int`
20
+ - `param: float = 0.1`
21
+ - `seed: int = 1`
22
+ - Returns an in-memory `graph_id` and summary stats.
23
+
24
+ 4. **graph_info**
25
+ - Parameters:
26
+ - `graph_id: str`
27
+ - Returns node/edge counts and empty-state flag.
28
+
29
+ 5. **basic_metrics**
30
+ - Parameters:
31
+ - `graph_id: str`
32
+ - `sample_nodes: int = 100`
33
+ - Returns triads, clustering coefficient, and approximate diameter.
34
+
35
+ 6. **connectivity_metrics**
36
+ - Parameters:
37
+ - `graph_id: str`
38
+ - `max_items: int = 20`
39
+ - Returns largest component fractions plus WCC/SCC distributions.
40
+
41
+ 7. **degree_distribution**
42
+ - Parameters:
43
+ - `graph_id: str`
44
+ - `max_items: int = 30`
45
+ - Returns sampled `(degree, count)` pairs.
46
+
47
+ 8. **call_snap_function**
48
+ - Parameters:
49
+ - `module_name: str`
50
+ - `function_name: str`
51
+ - `args_json: str = "[]"`
52
+ - `kwargs_json: str = "{}"`
53
+ - Generic dynamic executor for loaded modules.
54
+
55
+ All tool responses follow:
56
+
57
+ ```json
58
+ {
59
+ "success": true,
60
+ "result": {},
61
+ "error": null
62
+ }
63
+ ```
64
+
65
+ ## Example Usage
66
+
67
+ 1. Generate a graph:
68
+
69
+ ```json
70
+ {
71
+ "tool": "generate_graph",
72
+ "arguments": {
73
+ "model": "rmat",
74
+ "num_nodes": 10000,
75
+ "num_edges": 50000,
76
+ "param": 0.1,
77
+ "seed": 42
78
+ }
79
+ }
80
+ ```
81
+
82
+ 2. Compute metrics:
83
+
84
+ ```json
85
+ {
86
+ "tool": "basic_metrics",
87
+ "arguments": {
88
+ "graph_id": "<returned-graph-id>",
89
+ "sample_nodes": 200
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Run Locally (stdio)
95
+
96
+ ```bash
97
+ cd snap-python/mcp_output
98
+ python start_mcp.py
99
+ ```
100
+
101
+ or:
102
+
103
+ ```bash
104
+ cd snap-python/mcp_output/mcp_plugin
105
+ python main.py
106
+ ```
107
+
108
+ ## Run via HTTP Transport
109
+
110
+ ```bash
111
+ cd snap-python/mcp_output
112
+ MCP_TRANSPORT=http MCP_PORT=8000 python start_mcp.py
113
+ ```
114
+
115
+ The MCP HTTP endpoint is served at `/mcp`.
snap-python/mcp_output/mcp_plugin/adapter.py CHANGED
@@ -1,383 +1,180 @@
1
- import os
2
- import sys
3
  import importlib
4
- import importlib.util
5
- from typing import Any, Dict, Optional, Tuple
 
 
 
6
 
7
- source_path = os.path.join(
8
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
9
- "source",
10
- )
11
- sys.path.insert(0, source_path)
12
 
 
 
 
 
 
 
13
 
14
- class Adapter:
15
- """
16
- Import-mode adapter for snap-python repository integration.
17
 
18
- This adapter attempts to import repository-local modules discovered by analysis and
19
- exposes callable wrappers with unified response dictionaries.
20
 
21
- All public methods return:
22
- {
23
- "status": "success" | "error" | "fallback",
24
- "mode": "import" | "fallback",
25
- ...
26
- }
27
- """
28
 
29
- # -------------------------------------------------------------------------
30
- # Lifecycle and module management
31
- # -------------------------------------------------------------------------
32
  def __init__(self) -> None:
33
- """
34
- Initialize adapter in import mode and attempt best-effort module loading.
35
- """
36
- self.mode = "import"
37
- self._modules: Dict[str, Any] = {}
38
- self._import_errors: Dict[str, str] = {}
39
- self._load_modules()
40
-
41
- def _result(self, status: str, **kwargs: Any) -> Dict[str, Any]:
42
- data = {"status": status, "mode": self.mode}
43
- data.update(kwargs)
44
- return data
45
-
46
- def _safe_import(self, key: str, module_path: str) -> None:
47
- try:
48
- self._modules[key] = importlib.import_module(module_path)
49
- except Exception as e:
50
- self._modules[key] = None
51
- self._import_errors[key] = (
52
- f"Failed to import '{module_path}'. "
53
- f"Ensure repository source is available under '{source_path}' and dependencies are installed. "
54
- f"Details: {e}"
55
- )
56
-
57
- def _load_hyphen_module(self, key: str, relative_path: str) -> None:
58
- abs_path = os.path.join(source_path, relative_path)
59
- try:
60
- if not os.path.exists(abs_path):
61
- raise FileNotFoundError(
62
- f"Module file not found at '{abs_path}'. Check repository extraction path."
63
- )
64
- spec = importlib.util.spec_from_file_location(key, abs_path)
65
- if spec is None or spec.loader is None:
66
- raise ImportError(
67
- f"Could not create import spec for '{abs_path}'."
68
- )
69
- module = importlib.util.module_from_spec(spec)
70
- spec.loader.exec_module(module)
71
- self._modules[key] = module
72
- except Exception as e:
73
- self._modules[key] = None
74
- self._import_errors[key] = (
75
- f"Failed to load module from '{abs_path}'. "
76
- f"Verify file presence and Python compatibility. Details: {e}"
77
- )
78
-
79
- def _load_modules(self) -> None:
80
- """
81
- Load all discovered modules/classes/functions from analysis.
82
- """
83
- # Classes in setup modules
84
- self._safe_import("setup_setup", "setup")
85
- self._safe_import("swig_setup", "swig.setup")
86
-
87
- # Functions in standard importable module
88
- self._safe_import("genClassFnExt", "swig.gen.genClassFn.archive.genClassFnExt")
89
-
90
- # Functions in modules with hyphenated filenames (manual load)
91
- self._load_hyphen_module("disp_custom", os.path.join("swig", "gen", "disp-custom.py"))
92
- self._load_hyphen_module("tneanet_cpp", os.path.join("dev", "examples", "tneanet-cpp.py"))
93
- self._load_hyphen_module("snapswig_check", os.path.join("dev", "examples", "snapswig-check.py"))
94
-
95
- if self._import_errors:
96
- self.mode = "fallback"
97
-
98
- def health_check(self) -> Dict[str, Any]:
99
- """
100
- Return adapter/module import status.
101
-
102
- Returns:
103
- dict: Unified status payload with loaded module keys and import errors.
104
- """
105
- loaded = [k for k, v in self._modules.items() if v is not None]
106
- return self._result(
107
- "success" if not self._import_errors else "fallback",
108
- loaded_modules=loaded,
109
- import_errors=self._import_errors,
110
- guidance=(
111
- "If imports failed, ensure the repository is present under the configured source path, "
112
- "and install optional build/runtime dependencies for SWIG-based modules."
113
- ),
114
- )
115
-
116
- # -------------------------------------------------------------------------
117
- # Class instance creators
118
- # -------------------------------------------------------------------------
119
- def create_setup_swigbuild(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
120
- """
121
- Create instance of setup.SwigBuild.
122
-
123
- Parameters:
124
- *args: Positional arguments forwarded to setup.SwigBuild constructor.
125
- **kwargs: Keyword arguments forwarded to setup.SwigBuild constructor.
126
-
127
- Returns:
128
- dict: status and created instance or actionable error.
129
- """
130
- module = self._modules.get("setup_setup")
131
- if module is None or not hasattr(module, "SwigBuild"):
132
- return self._result(
133
- "fallback",
134
- error="Class 'SwigBuild' in module 'setup' is unavailable.",
135
- guidance="Ensure 'setup.py' is importable and supports runtime class instantiation.",
136
- )
137
- try:
138
- instance = module.SwigBuild(*args, **kwargs)
139
- return self._result("success", class_name="SwigBuild", instance=instance)
140
- except Exception as e:
141
- return self._result(
142
- "error",
143
- error=f"Failed to instantiate setup.SwigBuild: {e}",
144
- guidance="Check constructor arguments required by distutils/setuptools command classes.",
145
- )
146
-
147
- def create_setup_swigextension(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
148
- """
149
- Create instance of setup.SwigExtension.
150
- """
151
- module = self._modules.get("setup_setup")
152
- if module is None or not hasattr(module, "SwigExtension"):
153
- return self._result(
154
- "fallback",
155
- error="Class 'SwigExtension' in module 'setup' is unavailable.",
156
- guidance="Ensure setup module loads successfully.",
157
- )
158
- try:
159
- instance = module.SwigExtension(*args, **kwargs)
160
- return self._result("success", class_name="SwigExtension", instance=instance)
161
- except Exception as e:
162
- return self._result(
163
- "error",
164
- error=f"Failed to instantiate setup.SwigExtension: {e}",
165
- guidance="Provide valid extension constructor parameters (name, sources, and config).",
166
- )
167
-
168
- def create_swig_setup_pkgbuild(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
169
- """
170
- Create instance of swig.setup.PkgBuild.
171
- """
172
- module = self._modules.get("swig_setup")
173
- if module is None or not hasattr(module, "PkgBuild"):
174
- return self._result(
175
- "fallback",
176
- error="Class 'PkgBuild' in module 'swig.setup' is unavailable.",
177
- guidance="Ensure swig setup module is present and importable.",
178
- )
179
- try:
180
- instance = module.PkgBuild(*args, **kwargs)
181
- return self._result("success", class_name="PkgBuild", instance=instance)
182
- except Exception as e:
183
- return self._result(
184
- "error",
185
- error=f"Failed to instantiate swig.setup.PkgBuild: {e}",
186
- guidance="Verify arguments expected by build command classes.",
187
- )
188
-
189
- def create_swig_setup_swigextension(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
190
- """
191
- Create instance of swig.setup.SwigExtension.
192
- """
193
- module = self._modules.get("swig_setup")
194
- if module is None or not hasattr(module, "SwigExtension"):
195
- return self._result(
196
- "fallback",
197
- error="Class 'SwigExtension' in module 'swig.setup' is unavailable.",
198
- guidance="Ensure swig setup module import succeeds.",
199
- )
200
- try:
201
- instance = module.SwigExtension(*args, **kwargs)
202
- return self._result("success", class_name="SwigExtension", instance=instance)
203
- except Exception as e:
204
- return self._result(
205
- "error",
206
- error=f"Failed to instantiate swig.setup.SwigExtension: {e}",
207
- guidance="Pass valid extension initialization arguments.",
208
- )
209
-
210
- # -------------------------------------------------------------------------
211
- # Function call wrappers
212
- # -------------------------------------------------------------------------
213
- def call_convert_esubgraph(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
214
- """
215
- Call swig.gen.disp-custom.ConvertESubGraph.
216
-
217
- Parameters:
218
- *args: Forwarded to ConvertESubGraph.
219
- **kwargs: Forwarded to ConvertESubGraph.
220
- """
221
- module = self._modules.get("disp_custom")
222
- if module is None or not hasattr(module, "ConvertESubGraph"):
223
- return self._result(
224
- "fallback",
225
- error="Function 'ConvertESubGraph' is unavailable.",
226
- guidance="Check swig/gen/disp-custom.py availability and compatibility.",
227
- )
228
- try:
229
- result = module.ConvertESubGraph(*args, **kwargs)
230
- return self._result("success", function="ConvertESubGraph", result=result)
231
- except Exception as e:
232
- return self._result(
233
- "error",
234
- error=f"ConvertESubGraph execution failed: {e}",
235
- guidance="Validate argument types and graph object compatibility.",
236
- )
237
-
238
- def call_convert_graph(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
239
- """
240
- Call swig.gen.disp-custom.ConvertGraph.
241
- """
242
- module = self._modules.get("disp_custom")
243
- if module is None or not hasattr(module, "ConvertGraph"):
244
- return self._result(
245
- "fallback",
246
- error="Function 'ConvertGraph' is unavailable.",
247
- guidance="Check swig/gen/disp-custom.py load status.",
248
- )
249
- try:
250
- result = module.ConvertGraph(*args, **kwargs)
251
- return self._result("success", function="ConvertGraph", result=result)
252
- except Exception as e:
253
- return self._result(
254
- "error",
255
- error=f"ConvertGraph execution failed: {e}",
256
- guidance="Ensure provided graph parameters match expected SNAP/SWIG types.",
257
- )
258
-
259
- def call_convert_subgraph(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
260
- """
261
- Call swig.gen.disp-custom.ConvertSubGraph.
262
- """
263
- module = self._modules.get("disp_custom")
264
- if module is None or not hasattr(module, "ConvertSubGraph"):
265
- return self._result(
266
- "fallback",
267
- error="Function 'ConvertSubGraph' is unavailable.",
268
- guidance="Check swig/gen/disp-custom.py and repository source path.",
269
- )
270
- try:
271
- result = module.ConvertSubGraph(*args, **kwargs)
272
- return self._result("success", function="ConvertSubGraph", result=result)
273
- except Exception as e:
274
- return self._result(
275
- "error",
276
- error=f"ConvertSubGraph execution failed: {e}",
277
- guidance="Review node/edge subset arguments and graph input types.",
278
- )
279
-
280
- def call_gen_func_call(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
281
- """
282
- Call swig.gen.genClassFn.archive.genClassFnExt.genFuncCall.
283
- """
284
- module = self._modules.get("genClassFnExt")
285
- if module is None or not hasattr(module, "genFuncCall"):
286
- return self._result(
287
- "fallback",
288
- error="Function 'genFuncCall' is unavailable.",
289
- guidance="Ensure module swig.gen.genClassFn.archive.genClassFnExt imports correctly.",
290
- )
291
- try:
292
- result = module.genFuncCall(*args, **kwargs)
293
- return self._result("success", function="genFuncCall", result=result)
294
- except Exception as e:
295
- return self._result(
296
- "error",
297
- error=f"genFuncCall execution failed: {e}",
298
- guidance="Verify expected inputs for class/function generation utility.",
299
- )
300
-
301
- def call_get_func_name(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
302
- """
303
- Call swig.gen.genClassFn.archive.genClassFnExt.getFuncName.
304
- """
305
- module = self._modules.get("genClassFnExt")
306
- if module is None or not hasattr(module, "getFuncName"):
307
- return self._result(
308
- "fallback",
309
- error="Function 'getFuncName' is unavailable.",
310
- guidance="Check import status for swig.gen.genClassFn.archive.genClassFnExt.",
311
- )
312
- try:
313
- result = module.getFuncName(*args, **kwargs)
314
- return self._result("success", function="getFuncName", result=result)
315
- except Exception as e:
316
- return self._result(
317
- "error",
318
- error=f"getFuncName execution failed: {e}",
319
- guidance="Provide input matching expected parser/line format.",
320
- )
321
-
322
- def call_remove_first_param(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
323
- """
324
- Call swig.gen.genClassFn.archive.genClassFnExt.removeFirstParam.
325
- """
326
- module = self._modules.get("genClassFnExt")
327
- if module is None or not hasattr(module, "removeFirstParam"):
328
- return self._result(
329
- "fallback",
330
- error="Function 'removeFirstParam' is unavailable.",
331
- guidance="Ensure genClassFnExt module loaded without syntax/runtime issues.",
332
- )
333
- try:
334
- result = module.removeFirstParam(*args, **kwargs)
335
- return self._result("success", function="removeFirstParam", result=result)
336
- except Exception as e:
337
- return self._result(
338
- "error",
339
- error=f"removeFirstParam execution failed: {e}",
340
- guidance="Check parameter string structure before invoking this utility.",
341
- )
342
 
343
- def call_tneanet_cpp_main(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
344
- """
345
- Call dev.examples.tneanet-cpp.main.
346
- """
347
- module = self._modules.get("tneanet_cpp")
348
- if module is None or not hasattr(module, "main"):
349
- return self._result(
350
- "fallback",
351
- error="Function 'main' in tneanet-cpp is unavailable.",
352
- guidance="Check file dev/examples/tneanet-cpp.py exists and is valid.",
353
- )
354
  try:
355
- result = module.main(*args, **kwargs)
356
- return self._result("success", function="tneanet_cpp.main", result=result)
357
- except Exception as e:
358
- return self._result(
359
- "error",
360
- error=f"tneanet_cpp.main execution failed: {e}",
361
- guidance="Confirm runtime prerequisites for example script are met.",
362
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
- def call_snapswig_check_main(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
365
- """
366
- Call dev.examples.snapswig-check.main.
367
- """
368
- module = self._modules.get("snapswig_check")
369
- if module is None or not hasattr(module, "main"):
370
- return self._result(
371
- "fallback",
372
- error="Function 'main' in snapswig-check is unavailable.",
373
- guidance="Check file dev/examples/snapswig-check.py exists and imports succeed.",
374
- )
375
  try:
376
- result = module.main(*args, **kwargs)
377
- return self._result("success", function="snapswig_check.main", result=result)
378
- except Exception as e:
379
- return self._result(
380
- "error",
381
- error=f"snapswig_check.main execution failed: {e}",
382
- guidance="Ensure SWIG SNAP extension is built and importable before running checks.",
383
- )
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
  import importlib
4
+ import pkgutil
5
+ import sys
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+ from typing import Any
9
 
 
 
 
 
 
10
 
11
+ def _prepend_source_path() -> Path:
12
+ plugin_dir = Path(__file__).resolve().parent
13
+ source_dir = plugin_dir.parent.parent / "source"
14
+ if source_dir.exists() and str(source_dir) not in sys.path:
15
+ sys.path.insert(0, str(source_dir))
16
+ return source_dir
17
 
 
 
 
18
 
19
+ SOURCE_DIR = _prepend_source_path()
 
20
 
 
 
 
 
 
 
 
21
 
22
+ class Adapter:
 
 
23
  def __init__(self) -> None:
24
+ self.loaded_modules: dict[str, ModuleType] = {}
25
+ self.failed_modules: dict[str, str] = {}
26
+ self.mode = "ready"
27
+ self.source_dir = SOURCE_DIR
28
+ self._load_all_modules()
29
+
30
+ def _discover_candidate_modules(self) -> list[str]:
31
+ candidates = ["snap", "snapx"]
32
+ if self.source_dir.exists():
33
+ for module_info in pkgutil.iter_modules([str(self.source_dir)]):
34
+ name = module_info.name
35
+ if name.startswith("_"):
36
+ continue
37
+ if name not in candidates:
38
+ candidates.append(name)
39
+ return candidates
40
+
41
+ def _load_all_modules(self) -> None:
42
+ candidates = self._discover_candidate_modules()
43
+ for module_name in candidates:
44
+ try:
45
+ module = importlib.import_module(module_name)
46
+ self.loaded_modules[module_name] = module
47
+ except Exception as exc:
48
+ self.failed_modules[module_name] = str(exc)
49
+
50
+ if not self.loaded_modules:
51
+ self.mode = "blackbox"
52
+
53
+ def health(self) -> dict[str, Any]:
54
+ if self.loaded_modules:
55
+ return {
56
+ "status": "ok",
57
+ "mode": self.mode,
58
+ "loaded_count": len(self.loaded_modules),
59
+ "failed_count": len(self.failed_modules),
60
+ "source_dir": str(self.source_dir),
61
+ }
62
+ return {
63
+ "status": "fallback",
64
+ "mode": "blackbox",
65
+ "message": "No modules could be imported. Running in fallback mode.",
66
+ "loaded_count": 0,
67
+ "failed_count": len(self.failed_modules),
68
+ "source_dir": str(self.source_dir),
69
+ }
70
+
71
+ def list_modules(self) -> dict[str, Any]:
72
+ status = "ok" if self.loaded_modules else "fallback"
73
+ return {
74
+ "status": status,
75
+ "mode": self.mode,
76
+ "loaded": sorted(self.loaded_modules.keys()),
77
+ "failed": self.failed_modules,
78
+ }
79
+
80
+ def list_symbols(self, module_name: str, limit: int = 200) -> dict[str, Any]:
81
+ module = self.loaded_modules.get(module_name)
82
+ if module is None:
83
+ return {
84
+ "status": "error",
85
+ "message": f"Module '{module_name}' is not loaded",
86
+ }
87
+
88
+ names = [name for name in dir(module) if not name.startswith("__")]
89
+ return {
90
+ "status": "ok",
91
+ "module": module_name,
92
+ "count": len(names),
93
+ "symbols": names[: max(1, limit)],
94
+ }
95
+
96
+ def call_function(
97
+ self,
98
+ module_name: str,
99
+ function_name: str,
100
+ args: list[Any] | None = None,
101
+ kwargs: dict[str, Any] | None = None,
102
+ ) -> dict[str, Any]:
103
+ module = self.loaded_modules.get(module_name)
104
+ if module is None:
105
+ return {
106
+ "status": "error",
107
+ "message": f"Module '{module_name}' is not loaded",
108
+ }
109
+
110
+ fn = getattr(module, function_name, None)
111
+ if fn is None or not callable(fn):
112
+ return {
113
+ "status": "error",
114
+ "message": f"Function '{function_name}' not found in module '{module_name}'",
115
+ }
116
+
117
+ safe_args = args if args is not None else []
118
+ safe_kwargs = kwargs if kwargs is not None else {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
 
 
 
 
 
 
 
 
 
 
 
120
  try:
121
+ result = fn(*safe_args, **safe_kwargs)
122
+ return {
123
+ "status": "ok",
124
+ "module": module_name,
125
+ "function": function_name,
126
+ "result": result,
127
+ }
128
+ except Exception as exc:
129
+ return {
130
+ "status": "error",
131
+ "module": module_name,
132
+ "function": function_name,
133
+ "message": str(exc),
134
+ }
135
+
136
+ def create_instance(
137
+ self,
138
+ module_name: str,
139
+ class_name: str,
140
+ args: list[Any] | None = None,
141
+ kwargs: dict[str, Any] | None = None,
142
+ ) -> dict[str, Any]:
143
+ module = self.loaded_modules.get(module_name)
144
+ if module is None:
145
+ return {
146
+ "status": "error",
147
+ "message": f"Module '{module_name}' is not loaded",
148
+ }
149
+
150
+ cls = getattr(module, class_name, None)
151
+ if cls is None:
152
+ return {
153
+ "status": "error",
154
+ "message": f"Class '{class_name}' not found in module '{module_name}'",
155
+ }
156
+
157
+ if not isinstance(cls, type):
158
+ return {
159
+ "status": "error",
160
+ "message": f"Symbol '{class_name}' in module '{module_name}' is not a class",
161
+ }
162
+
163
+ safe_args = args if args is not None else []
164
+ safe_kwargs = kwargs if kwargs is not None else {}
165
 
 
 
 
 
 
 
 
 
 
 
 
166
  try:
167
+ instance = cls(*safe_args, **safe_kwargs)
168
+ return {
169
+ "status": "ok",
170
+ "module": module_name,
171
+ "class": class_name,
172
+ "instance_type": type(instance).__name__,
173
+ }
174
+ except Exception as exc:
175
+ return {
176
+ "status": "error",
177
+ "module": module_name,
178
+ "class": class_name,
179
+ "message": str(exc),
180
+ }
snap-python/mcp_output/mcp_plugin/main.py CHANGED
@@ -1,13 +1,11 @@
1
- """
2
- MCP Service Auto-Wrapper - Auto-generated
3
- """
4
  from mcp_service import create_app
5
 
6
- def main():
7
- """Main entry point"""
 
8
  app = create_app()
9
- return app
 
10
 
11
  if __name__ == "__main__":
12
- app = main()
13
- app.run()
 
 
 
 
1
  from mcp_service import create_app
2
 
3
+
4
+ def main() -> None:
5
+ # Local stdio entry point only (Claude Desktop / CLI), not for Docker/web deployment.
6
  app = create_app()
7
+ app.run(transport="stdio")
8
+
9
 
10
  if __name__ == "__main__":
11
+ main()
 
snap-python/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,271 +1,340 @@
 
 
 
1
  import os
2
  import sys
3
-
4
- source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
5
- if source_path not in sys.path:
6
- sys.path.insert(0, source_path)
7
 
8
  from fastmcp import FastMCP
9
 
10
- from setup import SwigExtension, SwigBuild
11
- from swig.setup import SwigExtension, PkgBuild
12
- from swig.gen.disp-custom import ConvertGraph, ConvertSubGraph, ConvertESubGraph
13
- from swig.gen.genClassFn.archive.genClassFnExt import removeFirstParam, genFuncCall, getFuncName
14
- from dev.examples.tneanet-cpp import main
15
- from dev.examples.snapswig-check import main
16
 
17
- mcp = FastMCP("unknown_service")
18
 
 
 
 
 
 
 
19
 
20
- @mcp.tool(name="swigbuild", description="SwigBuild class")
21
- def swigbuild(*args, **kwargs):
22
- """SwigBuild class"""
23
- try:
24
- if SwigBuild is None:
25
- return {"success": False, "result": None, "error": "Class SwigBuild is not available, path may need adjustment"}
26
-
27
- # MCP parameter type conversion
28
- converted_args = []
29
- converted_kwargs = kwargs.copy()
30
-
31
- # Handle position argument type conversion
32
- for arg in args:
33
- if isinstance(arg, str):
34
- # Try to convert to numeric type
35
- try:
36
- if '.' in arg:
37
- converted_args.append(float(arg))
38
- else:
39
- converted_args.append(int(arg))
40
- except ValueError:
41
- converted_args.append(arg)
42
- else:
43
- converted_args.append(arg)
44
-
45
- # Handle keyword argument type conversion
46
- for key, value in converted_kwargs.items():
47
- if isinstance(value, str):
48
- try:
49
- if '.' in value:
50
- converted_kwargs[key] = float(value)
51
- else:
52
- converted_kwargs[key] = int(value)
53
- except ValueError:
54
- pass
55
-
56
- instance = SwigBuild(*converted_args, **converted_kwargs)
57
- return {"success": True, "result": str(instance), "error": None}
58
- except Exception as e:
59
- return {"success": False, "result": None, "error": str(e)}
60
-
61
- @mcp.tool(name="swigextension", description="SwigExtension class")
62
- def swigextension(*args, **kwargs):
63
- """SwigExtension class"""
64
- try:
65
- if SwigExtension is None:
66
- return {"success": False, "result": None, "error": "Class SwigExtension is not available, path may need adjustment"}
67
-
68
- # MCP parameter type conversion
69
- converted_args = []
70
- converted_kwargs = kwargs.copy()
71
-
72
- # Handle position argument type conversion
73
- for arg in args:
74
- if isinstance(arg, str):
75
- # Try to convert to numeric type
76
- try:
77
- if '.' in arg:
78
- converted_args.append(float(arg))
79
- else:
80
- converted_args.append(int(arg))
81
- except ValueError:
82
- converted_args.append(arg)
83
- else:
84
- converted_args.append(arg)
85
-
86
- # Handle keyword argument type conversion
87
- for key, value in converted_kwargs.items():
88
- if isinstance(value, str):
89
- try:
90
- if '.' in value:
91
- converted_kwargs[key] = float(value)
92
- else:
93
- converted_kwargs[key] = int(value)
94
- except ValueError:
95
- pass
96
-
97
- instance = SwigExtension(*converted_args, **converted_kwargs)
98
- return {"success": True, "result": str(instance), "error": None}
99
- except Exception as e:
100
- return {"success": False, "result": None, "error": str(e)}
101
-
102
- @mcp.tool(name="pkgbuild", description="PkgBuild class")
103
- def pkgbuild(*args, **kwargs):
104
- """PkgBuild class"""
105
- try:
106
- if PkgBuild is None:
107
- return {"success": False, "result": None, "error": "Class PkgBuild is not available, path may need adjustment"}
108
-
109
- # MCP parameter type conversion
110
- converted_args = []
111
- converted_kwargs = kwargs.copy()
112
-
113
- # Handle position argument type conversion
114
- for arg in args:
115
- if isinstance(arg, str):
116
- # Try to convert to numeric type
117
- try:
118
- if '.' in arg:
119
- converted_args.append(float(arg))
120
- else:
121
- converted_args.append(int(arg))
122
- except ValueError:
123
- converted_args.append(arg)
124
- else:
125
- converted_args.append(arg)
126
-
127
- # Handle keyword argument type conversion
128
- for key, value in converted_kwargs.items():
129
- if isinstance(value, str):
130
- try:
131
- if '.' in value:
132
- converted_kwargs[key] = float(value)
133
- else:
134
- converted_kwargs[key] = int(value)
135
- except ValueError:
136
- pass
137
-
138
- instance = PkgBuild(*converted_args, **converted_kwargs)
139
- return {"success": True, "result": str(instance), "error": None}
140
- except Exception as e:
141
- return {"success": False, "result": None, "error": str(e)}
142
-
143
- @mcp.tool(name="swigextension", description="SwigExtension class")
144
- def swigextension(*args, **kwargs):
145
- """SwigExtension class"""
146
  try:
147
- if SwigExtension is None:
148
- return {"success": False, "result": None, "error": "Class SwigExtension is not available, path may need adjustment"}
149
-
150
- # MCP parameter type conversion
151
- converted_args = []
152
- converted_kwargs = kwargs.copy()
153
-
154
- # Handle position argument type conversion
155
- for arg in args:
156
- if isinstance(arg, str):
157
- # Try to convert to numeric type
158
- try:
159
- if '.' in arg:
160
- converted_args.append(float(arg))
161
- else:
162
- converted_args.append(int(arg))
163
- except ValueError:
164
- converted_args.append(arg)
165
- else:
166
- converted_args.append(arg)
167
-
168
- # Handle keyword argument type conversion
169
- for key, value in converted_kwargs.items():
170
- if isinstance(value, str):
171
- try:
172
- if '.' in value:
173
- converted_kwargs[key] = float(value)
174
- else:
175
- converted_kwargs[key] = int(value)
176
- except ValueError:
177
- pass
178
-
179
- instance = SwigExtension(*converted_args, **converted_kwargs)
180
- return {"success": True, "result": str(instance), "error": None}
181
- except Exception as e:
182
- return {"success": False, "result": None, "error": str(e)}
183
-
184
- @mcp.tool(name="ConvertESubGraph", description="Auto-wrapped function ConvertESubGraph")
185
- def ConvertESubGraph(payload: dict):
186
  try:
187
- if ConvertESubGraph is None:
188
- return {"success": False, "result": None, "error": "Function ConvertESubGraph is not available"}
189
- result = ConvertESubGraph(**payload)
190
- return {"success": True, "result": result, "error": None}
191
- except Exception as e:
192
- return {"success": False, "result": None, "error": str(e)}
193
-
194
- @mcp.tool(name="ConvertGraph", description="Auto-wrapped function ConvertGraph")
195
- def ConvertGraph(payload: dict):
 
 
 
 
196
  try:
197
- if ConvertGraph is None:
198
- return {"success": False, "result": None, "error": "Function ConvertGraph is not available"}
199
- result = ConvertGraph(**payload)
200
- return {"success": True, "result": result, "error": None}
201
- except Exception as e:
202
- return {"success": False, "result": None, "error": str(e)}
203
-
204
- @mcp.tool(name="ConvertSubGraph", description="Auto-wrapped function ConvertSubGraph")
205
- def ConvertSubGraph(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  try:
207
- if ConvertSubGraph is None:
208
- return {"success": False, "result": None, "error": "Function ConvertSubGraph is not available"}
209
- result = ConvertSubGraph(**payload)
210
- return {"success": True, "result": result, "error": None}
211
- except Exception as e:
212
- return {"success": False, "result": None, "error": str(e)}
213
-
214
- @mcp.tool(name="genFuncCall", description="Auto-wrapped function genFuncCall")
215
- def genFuncCall(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  try:
217
- if genFuncCall is None:
218
- return {"success": False, "result": None, "error": "Function genFuncCall is not available"}
219
- result = genFuncCall(**payload)
220
- return {"success": True, "result": result, "error": None}
221
- except Exception as e:
222
- return {"success": False, "result": None, "error": str(e)}
223
-
224
- @mcp.tool(name="getFuncName", description="Auto-wrapped function getFuncName")
225
- def getFuncName(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  try:
227
- if getFuncName is None:
228
- return {"success": False, "result": None, "error": "Function getFuncName is not available"}
229
- result = getFuncName(**payload)
230
- return {"success": True, "result": result, "error": None}
231
- except Exception as e:
232
- return {"success": False, "result": None, "error": str(e)}
233
-
234
- @mcp.tool(name="removeFirstParam", description="Auto-wrapped function removeFirstParam")
235
- def removeFirstParam(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  try:
237
- if removeFirstParam is None:
238
- return {"success": False, "result": None, "error": "Function removeFirstParam is not available"}
239
- result = removeFirstParam(**payload)
240
- return {"success": True, "result": result, "error": None}
241
- except Exception as e:
242
- return {"success": False, "result": None, "error": str(e)}
243
-
244
- @mcp.tool(name="main", description="Auto-wrapped function main")
245
- def main(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  try:
247
- if main is None:
248
- return {"success": False, "result": None, "error": "Function main is not available"}
249
- result = main(**payload)
250
- return {"success": True, "result": result, "error": None}
251
- except Exception as e:
252
- return {"success": False, "result": None, "error": str(e)}
253
-
254
- @mcp.tool(name="main", description="Auto-wrapped function main")
255
- def main(payload: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  try:
257
- if main is None:
258
- return {"success": False, "result": None, "error": "Function main is not available"}
259
- result = main(**payload)
260
- return {"success": True, "result": result, "error": None}
261
- except Exception as e:
262
- return {"success": False, "result": None, "error": str(e)}
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
 
266
- def create_app():
267
- """Create and return FastMCP application instance"""
268
  return mcp
269
 
 
270
  if __name__ == "__main__":
271
- mcp.run(transport="http", host="0.0.0.0", port=8000)
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
  import os
5
  import sys
6
+ import uuid
7
+ from pathlib import Path
8
+ from typing import Any
 
9
 
10
  from fastmcp import FastMCP
11
 
12
+ from adapter import Adapter
 
 
 
 
 
13
 
 
14
 
15
+ def _prepend_source_path() -> Path:
16
+ plugin_dir = Path(__file__).resolve().parent
17
+ source_dir = plugin_dir.parent.parent / "source"
18
+ if source_dir.exists() and str(source_dir) not in sys.path:
19
+ sys.path.insert(0, str(source_dir))
20
+ return source_dir
21
 
22
+
23
+ SOURCE_DIR = _prepend_source_path()
24
+
25
+ mcp = FastMCP("snap-python-mcp")
26
+ adapter = Adapter()
27
+
28
+ GRAPH_STORE: dict[str, Any] = {}
29
+
30
+
31
+ def _ok(result: Any) -> dict[str, Any]:
32
+ return {"success": True, "result": result, "error": None}
33
+
34
+
35
+ def _err(message: str) -> dict[str, Any]:
36
+ return {"success": False, "result": None, "error": message}
37
+
38
+
39
+ def _get_snap_module() -> Any:
40
+ snap_module = adapter.loaded_modules.get("snap")
41
+ if snap_module is not None:
42
+ return snap_module
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  try:
44
+ import snap as snap_module # type: ignore
45
+
46
+ return snap_module
47
+ except Exception as exc:
48
+ raise RuntimeError(f"Failed to import 'snap': {exc}") from exc
49
+
50
+
51
+ def _get_graph(graph_id: str) -> Any:
52
+ graph = GRAPH_STORE.get(graph_id)
53
+ if graph is None:
54
+ raise KeyError(f"Graph id '{graph_id}' not found")
55
+ return graph
56
+
57
+
58
+ def _int_pair_vector_to_list(vector_obj: Any, max_items: int) -> list[dict[str, int]]:
59
+ items: list[dict[str, int]] = []
60
+ length = int(getattr(vector_obj, "Len")())
61
+ for index in range(min(length, max_items)):
62
+ pair = vector_obj.GetI(index)
63
+ value1 = int(pair.GetVal1())
64
+ value2 = int(pair.GetVal2())
65
+ items.append({"x": value1, "count": value2})
66
+ return items
67
+
68
+
69
+ @mcp.tool(name="adapter_health", description="Get adapter and module loading health status")
70
+ def adapter_health() -> dict[str, Any]:
71
+ """Return adapter health.
72
+
73
+ Returns:
74
+ A standardized MCP response with adapter mode, loaded module counts,
75
+ and source path diagnostics.
76
+ """
 
 
 
 
 
 
77
  try:
78
+ result = adapter.health()
79
+ return _ok(result)
80
+ except Exception as exc:
81
+ return _err(str(exc))
82
+
83
+
84
+ @mcp.tool(name="list_modules", description="List successfully loaded and failed modules")
85
+ def list_modules() -> dict[str, Any]:
86
+ """List loaded modules.
87
+
88
+ Returns:
89
+ A standardized MCP response containing loaded and failed module maps.
90
+ """
91
  try:
92
+ return _ok(adapter.list_modules())
93
+ except Exception as exc:
94
+ return _err(str(exc))
95
+
96
+
97
+ @mcp.tool(name="generate_graph", description="Generate a SNAP graph and keep it in an in-memory store")
98
+ def generate_graph(
99
+ model: str,
100
+ num_nodes: int,
101
+ num_edges: int,
102
+ param: float = 0.1,
103
+ seed: int = 1,
104
+ ) -> dict[str, Any]:
105
+ """Generate a graph with SNAP and return a graph id.
106
+
107
+ Args:
108
+ model: Graph model name. One of: rnd_undirected, rnd_directed,
109
+ pref_attach, small_world, rmat, forest_fire.
110
+ num_nodes: Number of nodes to generate.
111
+ num_edges: Number of edges for models that require it.
112
+ param: Model-specific parameter. For `pref_attach`, this is node out-degree.
113
+ For `small_world`, this is rewiring probability.
114
+ For `forest_fire`, this is forward burn probability.
115
+ seed: Random seed for stochastic graph generation.
116
+
117
+ Returns:
118
+ A standardized MCP response with graph id and graph summary.
119
+ """
120
  try:
121
+ if num_nodes <= 0:
122
+ return _err("num_nodes must be > 0")
123
+
124
+ snap = _get_snap_module()
125
+ graph = None
126
+ normalized = model.strip().lower()
127
+
128
+ if normalized == "rnd_undirected":
129
+ if num_edges <= 0:
130
+ return _err("num_edges must be > 0 for rnd_undirected")
131
+ graph = snap.GenRndGnm(snap.PUNGraph, int(num_nodes), int(num_edges), int(seed))
132
+ elif normalized == "rnd_directed":
133
+ if num_edges <= 0:
134
+ return _err("num_edges must be > 0 for rnd_directed")
135
+ graph = snap.GenRndGnm(snap.PNGraph, int(num_nodes), int(num_edges), int(seed))
136
+ elif normalized == "pref_attach":
137
+ node_out_degree = max(1, int(param))
138
+ graph = snap.GenPrefAttach(int(num_nodes), int(node_out_degree))
139
+ elif normalized == "small_world":
140
+ node_out_degree = max(2, int(num_edges / max(1, num_nodes)))
141
+ rewire_prob = min(max(float(param), 0.0), 1.0)
142
+ graph = snap.GenSmallWorld(int(num_nodes), int(node_out_degree), rewire_prob)
143
+ elif normalized == "rmat":
144
+ if num_edges <= 0:
145
+ return _err("num_edges must be > 0 for rmat")
146
+ rnd = snap.TRnd(int(seed))
147
+ rnd.Randomize()
148
+ graph = snap.GenRMat(int(num_nodes), int(num_edges), 0.45, 0.15, 0.15, rnd)
149
+ elif normalized == "forest_fire":
150
+ burn_prob = min(max(float(param), 0.01), 0.99)
151
+ graph = snap.GenForestFire(int(num_nodes), burn_prob, burn_prob)
152
+ else:
153
+ return _err(
154
+ "Unsupported model. Use one of: rnd_undirected, rnd_directed, "
155
+ "pref_attach, small_world, rmat, forest_fire"
156
+ )
157
+
158
+ graph_id = str(uuid.uuid4())
159
+ GRAPH_STORE[graph_id] = graph
160
+ return _ok(
161
+ {
162
+ "graph_id": graph_id,
163
+ "model": normalized,
164
+ "nodes": int(graph.GetNodes()),
165
+ "edges": int(graph.GetEdges()),
166
+ }
167
+ )
168
+ except Exception as exc:
169
+ return _err(str(exc))
170
+
171
+
172
+ @mcp.tool(name="graph_info", description="Return node and edge counts for a stored graph")
173
+ def graph_info(graph_id: str) -> dict[str, Any]:
174
+ """Get basic graph summary.
175
+
176
+ Args:
177
+ graph_id: Identifier returned by `generate_graph`.
178
+
179
+ Returns:
180
+ A standardized MCP response with node/edge statistics.
181
+ """
182
  try:
183
+ graph = _get_graph(graph_id)
184
+ result = {
185
+ "graph_id": graph_id,
186
+ "nodes": int(graph.GetNodes()),
187
+ "edges": int(graph.GetEdges()),
188
+ "is_empty": bool(graph.Empty()),
189
+ }
190
+ return _ok(result)
191
+ except Exception as exc:
192
+ return _err(str(exc))
193
+
194
+
195
+ @mcp.tool(name="basic_metrics", description="Compute triads, clustering coefficient, and approximate diameter")
196
+ def basic_metrics(graph_id: str, sample_nodes: int = 100) -> dict[str, Any]:
197
+ """Compute baseline topology metrics for a graph.
198
+
199
+ Args:
200
+ graph_id: Identifier returned by `generate_graph`.
201
+ sample_nodes: Number of BFS samples used for approximate diameter.
202
+
203
+ Returns:
204
+ A standardized MCP response with triads, clustering, and diameter.
205
+ """
206
  try:
207
+ snap = _get_snap_module()
208
+ graph = _get_graph(graph_id)
209
+ triads = int(snap.GetTriads(graph))
210
+ clust_cf = float(snap.GetClustCf(graph))
211
+ diameter = int(snap.GetBfsFullDiam(graph, int(max(1, sample_nodes))))
212
+ return _ok(
213
+ {
214
+ "graph_id": graph_id,
215
+ "triads": triads,
216
+ "clustering_coefficient": clust_cf,
217
+ "approx_diameter": diameter,
218
+ }
219
+ )
220
+ except Exception as exc:
221
+ return _err(str(exc))
222
+
223
+
224
+ @mcp.tool(name="connectivity_metrics", description="Compute largest WCC/SCC and their size distributions")
225
+ def connectivity_metrics(graph_id: str, max_items: int = 20) -> dict[str, Any]:
226
+ """Compute connectivity-related metrics.
227
+
228
+ Args:
229
+ graph_id: Identifier returned by `generate_graph`.
230
+ max_items: Maximum number of distribution entries to return.
231
+
232
+ Returns:
233
+ A standardized MCP response with WCC/SCC summary and distributions.
234
+ """
235
  try:
236
+ snap = _get_snap_module()
237
+ graph = _get_graph(graph_id)
238
+
239
+ wcc_cnt = snap.TIntPrV()
240
+ scc_cnt = snap.TIntPrV()
241
+ snap.GetWccSzCnt(graph, wcc_cnt)
242
+ snap.GetSccSzCnt(graph, scc_cnt)
243
+
244
+ result = {
245
+ "graph_id": graph_id,
246
+ "max_wcc_fraction": float(snap.GetMxWccSz(graph)),
247
+ "max_scc_fraction": float(snap.GetMxSccSz(graph)),
248
+ "wcc_distribution": _int_pair_vector_to_list(wcc_cnt, max(1, max_items)),
249
+ "scc_distribution": _int_pair_vector_to_list(scc_cnt, max(1, max_items)),
250
+ }
251
+ return _ok(result)
252
+ except Exception as exc:
253
+ return _err(str(exc))
254
+
255
+
256
+ @mcp.tool(name="degree_distribution", description="Return out-degree distribution as (degree, count) pairs")
257
+ def degree_distribution(graph_id: str, max_items: int = 30) -> dict[str, Any]:
258
+ """Compute degree distribution for a stored graph.
259
+
260
+ Args:
261
+ graph_id: Identifier returned by `generate_graph`.
262
+ max_items: Maximum number of (degree, count) pairs to return.
263
+
264
+ Returns:
265
+ A standardized MCP response with degree histogram samples.
266
+ """
267
  try:
268
+ snap = _get_snap_module()
269
+ graph = _get_graph(graph_id)
270
+ deg_cnt = snap.TIntPrV()
271
+ snap.GetOutDegCnt(graph, deg_cnt)
272
+ return _ok(
273
+ {
274
+ "graph_id": graph_id,
275
+ "distribution": _int_pair_vector_to_list(deg_cnt, max(1, max_items)),
276
+ }
277
+ )
278
+ except Exception as exc:
279
+ return _err(str(exc))
280
+
281
+
282
+ @mcp.tool(name="call_snap_function", description="Call a selected function from a loaded module using JSON arguments")
283
+ def call_snap_function(
284
+ module_name: str,
285
+ function_name: str,
286
+ args_json: str = "[]",
287
+ kwargs_json: str = "{}",
288
+ ) -> dict[str, Any]:
289
+ """Call a dynamically selected function from loaded modules.
290
+
291
+ Args:
292
+ module_name: Module name (for example, `snap`).
293
+ function_name: Callable symbol name in the module.
294
+ args_json: JSON array string for positional arguments.
295
+ kwargs_json: JSON object string for keyword arguments.
296
+
297
+ Returns:
298
+ A standardized MCP response with dynamic call status and result metadata.
299
+ """
300
  try:
301
+ parsed_args = json.loads(args_json)
302
+ parsed_kwargs = json.loads(kwargs_json)
303
+ if not isinstance(parsed_args, list):
304
+ return _err("args_json must decode to a JSON array")
305
+ if not isinstance(parsed_kwargs, dict):
306
+ return _err("kwargs_json must decode to a JSON object")
307
 
308
+ response = adapter.call_function(
309
+ module_name=module_name,
310
+ function_name=function_name,
311
+ args=parsed_args,
312
+ kwargs=parsed_kwargs,
313
+ )
314
+ if response.get("status") == "ok":
315
+ raw = response.get("result")
316
+ safe_result = raw if isinstance(raw, (str, int, float, bool, list, dict, type(None))) else str(raw)
317
+ return _ok(
318
+ {
319
+ "status": "ok",
320
+ "module": module_name,
321
+ "function": function_name,
322
+ "result": safe_result,
323
+ }
324
+ )
325
+ return _err(response.get("message", "Dynamic call failed"))
326
+ except Exception as exc:
327
+ return _err(str(exc))
328
 
329
 
330
+ def create_app() -> FastMCP:
 
331
  return mcp
332
 
333
+
334
  if __name__ == "__main__":
335
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower() or "stdio"
336
+ if transport == "http":
337
+ port = int(os.getenv("MCP_PORT", "8000"))
338
+ mcp.run(transport="http", host="0.0.0.0", port=port)
339
+ else:
340
+ mcp.run(transport="stdio")
snap-python/mcp_output/requirements.txt CHANGED
@@ -1,5 +1,2 @@
1
  fastmcp
2
- fastapi
3
- uvicorn[standard]
4
- pydantic>=2.0.0
5
- snapx package modules under source.snapx.snapx
 
1
  fastmcp
2
+ snap-stanford
 
 
 
snap-python/mcp_output/start_mcp.py CHANGED
@@ -1,30 +1,28 @@
 
1
 
2
- """
3
- MCP Service Startup Entry
4
- """
5
- import sys
6
  import os
 
 
 
7
 
8
- project_root = os.path.dirname(os.path.abspath(__file__))
9
- mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
- if mcp_plugin_dir not in sys.path:
11
- sys.path.insert(0, mcp_plugin_dir)
12
 
13
  from mcp_service import create_app
14
 
15
- def main():
16
- """Start FastMCP service"""
 
 
17
  app = create_app()
18
- # Use environment variable to configure port, default 8000
19
- port = int(os.environ.get("MCP_PORT", "8000"))
20
-
21
- # Choose transport mode based on environment variable
22
- transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
  if transport == "http":
24
  app.run(transport="http", host="0.0.0.0", port=port)
25
  else:
26
- # Default to STDIO mode
27
- app.run()
28
 
29
  if __name__ == "__main__":
30
- main()
 
1
+ from __future__ import annotations
2
 
 
 
 
 
3
  import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
 
8
+ CURRENT_DIR = Path(__file__).resolve().parent
9
+ PLUGIN_DIR = CURRENT_DIR / "mcp_plugin"
10
+ if str(PLUGIN_DIR) not in sys.path:
11
+ sys.path.insert(0, str(PLUGIN_DIR))
12
 
13
  from mcp_service import create_app
14
 
15
+
16
+ def main() -> None:
17
+ transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower() or "stdio"
18
+ port = int(os.getenv("MCP_PORT", "8000"))
19
  app = create_app()
20
+
 
 
 
 
21
  if transport == "http":
22
  app.run(transport="http", host="0.0.0.0", port=port)
23
  else:
24
+ app.run(transport="stdio")
25
+
26
 
27
  if __name__ == "__main__":
28
+ main()