mesh-mcp 0.1.0__tar.gz
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.
- mesh_mcp-0.1.0/.gitignore +39 -0
- mesh_mcp-0.1.0/LICENSE +21 -0
- mesh_mcp-0.1.0/PKG-INFO +24 -0
- mesh_mcp-0.1.0/README.md +10 -0
- mesh_mcp-0.1.0/models.py +7 -0
- mesh_mcp-0.1.0/openapi.yaml +72 -0
- mesh_mcp-0.1.0/pyproject.toml +28 -0
- mesh_mcp-0.1.0/src/mesh_mcp/__init__.py +3 -0
- mesh_mcp-0.1.0/src/mesh_mcp/detectors/openapi.py +59 -0
- mesh_mcp-0.1.0/src/mesh_mcp/detectors/python_detector.py +35 -0
- mesh_mcp-0.1.0/src/mesh_mcp/detectors/typescript_detector.py +3 -0
- mesh_mcp-0.1.0/src/mesh_mcp/graph.py +3 -0
- mesh_mcp-0.1.0/src/mesh_mcp/indexer.py +3 -0
- mesh_mcp-0.1.0/src/mesh_mcp/server.py +138 -0
- mesh_mcp-0.1.0/tests/__init__.py +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
share/python-wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
|
|
24
|
+
# Virtual Environment
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
env/
|
|
29
|
+
|
|
30
|
+
# IDE / Editor
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
|
|
36
|
+
# Testing / Cache
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
mesh_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nagachaitanya Inamdar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mesh_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mesh-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cross-boundary contract blast-radius engine for LLM coding agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/Nagachaitanya007/mesh-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/Nagachaitanya007/mesh-mcp
|
|
7
|
+
Author-email: Nagachaitanya <nagachaitanyainamdar@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: mcp>=1.0.0
|
|
12
|
+
Requires-Dist: pyyaml>=6.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# mesh-mcp
|
|
16
|
+
|
|
17
|
+
Cross-boundary contract blast-radius engine for LLM coding agents.
|
|
18
|
+
|
|
19
|
+
Helps AI agents detect what breaks when you change an API contract or schema.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install mesh-mcp
|
mesh_mcp-0.1.0/README.md
ADDED
mesh_mcp-0.1.0/models.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
openapi: 3.0.3
|
|
2
|
+
info:
|
|
3
|
+
title: Orders API
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: Sample API for testing mesh-mcp
|
|
6
|
+
|
|
7
|
+
paths:
|
|
8
|
+
/orders:
|
|
9
|
+
get:
|
|
10
|
+
summary: List orders
|
|
11
|
+
responses:
|
|
12
|
+
'200':
|
|
13
|
+
description: A list of orders
|
|
14
|
+
content:
|
|
15
|
+
application/json:
|
|
16
|
+
schema:
|
|
17
|
+
type: array
|
|
18
|
+
items:
|
|
19
|
+
$ref: '#/components/schemas/Order'
|
|
20
|
+
post:
|
|
21
|
+
summary: Create an order
|
|
22
|
+
requestBody:
|
|
23
|
+
required: true
|
|
24
|
+
content:
|
|
25
|
+
application/json:
|
|
26
|
+
schema:
|
|
27
|
+
$ref: '#/components/schemas/Order'
|
|
28
|
+
responses:
|
|
29
|
+
'201':
|
|
30
|
+
description: Order created
|
|
31
|
+
|
|
32
|
+
/orders/{id}:
|
|
33
|
+
get:
|
|
34
|
+
summary: Get an order by ID
|
|
35
|
+
parameters:
|
|
36
|
+
- name: id
|
|
37
|
+
in: path
|
|
38
|
+
required: true
|
|
39
|
+
schema:
|
|
40
|
+
type: string
|
|
41
|
+
responses:
|
|
42
|
+
'200':
|
|
43
|
+
description: An order
|
|
44
|
+
content:
|
|
45
|
+
application/json:
|
|
46
|
+
schema:
|
|
47
|
+
$ref: '#/components/schemas/Order'
|
|
48
|
+
|
|
49
|
+
components:
|
|
50
|
+
schemas:
|
|
51
|
+
Order:
|
|
52
|
+
type: object
|
|
53
|
+
required:
|
|
54
|
+
- id
|
|
55
|
+
- total
|
|
56
|
+
properties:
|
|
57
|
+
id:
|
|
58
|
+
type: string
|
|
59
|
+
customer_id:
|
|
60
|
+
type: string
|
|
61
|
+
total:
|
|
62
|
+
type: number
|
|
63
|
+
format: float
|
|
64
|
+
currency:
|
|
65
|
+
type: string
|
|
66
|
+
enum: [USD, EUR, GBP]
|
|
67
|
+
tax_amount:
|
|
68
|
+
type: number
|
|
69
|
+
format: float
|
|
70
|
+
status:
|
|
71
|
+
type: string
|
|
72
|
+
enum: [pending, paid, shipped, cancelled]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mesh-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Cross-boundary contract blast-radius engine for LLM coding agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Nagachaitanya", email = "nagachaitanyainamdar@gmail.com"}
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mcp>=1.0.0",
|
|
13
|
+
"pyyaml>=6.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/Nagachaitanya007/mesh-mcp"
|
|
18
|
+
Repository = "https://github.com/Nagachaitanya007/mesh-mcp"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
mesh-mcp = "mesh_mcp.server:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/mesh_mcp"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""OpenAPI contract detector."""
|
|
2
|
+
|
|
3
|
+
# TODO: Parse OpenAPI / Swagger files
|
|
4
|
+
"""OpenAPI contract detector."""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import yaml
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def find_openapi_files(root: Path) -> list[Path]:
|
|
12
|
+
"""Find OpenAPI / Swagger files in the workspace."""
|
|
13
|
+
patterns = ["**/openapi.yaml", "**/openapi.yml", "**/openapi.json",
|
|
14
|
+
"**/swagger.yaml", "**/swagger.yml", "**/swagger.json"]
|
|
15
|
+
files = []
|
|
16
|
+
for pattern in patterns:
|
|
17
|
+
files.extend(root.glob(pattern))
|
|
18
|
+
# Remove duplicates and ignore common junk
|
|
19
|
+
unique = []
|
|
20
|
+
seen = set()
|
|
21
|
+
for f in files:
|
|
22
|
+
if f.resolve() not in seen and ".venv" not in str(f) and "node_modules" not in str(f):
|
|
23
|
+
seen.add(f.resolve())
|
|
24
|
+
unique.append(f)
|
|
25
|
+
return unique
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_openapi(file_path: Path) -> dict | None:
|
|
29
|
+
"""Parse an OpenAPI file and extract basic contract info."""
|
|
30
|
+
try:
|
|
31
|
+
text = file_path.read_text(encoding="utf-8")
|
|
32
|
+
if file_path.suffix in {".yaml", ".yml"}:
|
|
33
|
+
data = yaml.safe_load(text)
|
|
34
|
+
else:
|
|
35
|
+
data = json.loads(text)
|
|
36
|
+
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
info = {
|
|
41
|
+
"file": str(file_path),
|
|
42
|
+
"title": data.get("info", {}).get("title", file_path.stem),
|
|
43
|
+
"version": data.get("info", {}).get("version", "unknown"),
|
|
44
|
+
"schemas": list(data.get("components", {}).get("schemas", {}).keys()),
|
|
45
|
+
"paths": list(data.get("paths", {}).keys()),
|
|
46
|
+
}
|
|
47
|
+
return info
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_contracts(root: Path) -> list[dict]:
|
|
53
|
+
"""Find and parse all OpenAPI contracts in the workspace."""
|
|
54
|
+
contracts = []
|
|
55
|
+
for file_path in find_openapi_files(root):
|
|
56
|
+
parsed = parse_openapi(file_path)
|
|
57
|
+
if parsed:
|
|
58
|
+
contracts.append(parsed)
|
|
59
|
+
return contracts
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Simple Python consumer detector."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_python_consumers(root: Path, schema_names: list[str]) -> list[dict]:
|
|
8
|
+
"""
|
|
9
|
+
Find Python files that mention the given schema names.
|
|
10
|
+
This is a simple text-based detector for the MVP.
|
|
11
|
+
"""
|
|
12
|
+
consumers = []
|
|
13
|
+
ignore_dirs = {".venv", "venv", "__pycache__", ".git", "node_modules", "src"}
|
|
14
|
+
|
|
15
|
+
for py_file in root.rglob("*.py"):
|
|
16
|
+
if any(part in ignore_dirs for part in py_file.parts):
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
text = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
21
|
+
except Exception:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
for schema in schema_names:
|
|
25
|
+
# Simple check: class name or schema name appears
|
|
26
|
+
if re.search(rf"\b{schema}\b", text):
|
|
27
|
+
consumers.append({
|
|
28
|
+
"file": str(py_file),
|
|
29
|
+
"language": "python",
|
|
30
|
+
"schema": schema,
|
|
31
|
+
"reason": f"Mentions '{schema}'"
|
|
32
|
+
})
|
|
33
|
+
break # one match per file is enough for now
|
|
34
|
+
|
|
35
|
+
return consumers
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from mcp.server.mcpserver import MCPServer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from mesh_mcp.detectors.openapi import extract_contracts
|
|
6
|
+
from mesh_mcp.detectors.python_detector import find_python_consumers
|
|
7
|
+
|
|
8
|
+
mcp = MCPServer("mesh-mcp")
|
|
9
|
+
|
|
10
|
+
# Workspace
|
|
11
|
+
if len(sys.argv) > 1:
|
|
12
|
+
WORKSPACE = Path(sys.argv[1]).resolve()
|
|
13
|
+
else:
|
|
14
|
+
WORKSPACE = Path.cwd().resolve()
|
|
15
|
+
|
|
16
|
+
@mcp.tool()
|
|
17
|
+
def list_contracts() -> str:
|
|
18
|
+
"""
|
|
19
|
+
List all OpenAPI contracts found in the workspace.
|
|
20
|
+
Use this to see what APIs and schemas are available.
|
|
21
|
+
"""
|
|
22
|
+
contracts = extract_contracts(WORKSPACE)
|
|
23
|
+
if not contracts:
|
|
24
|
+
return "No OpenAPI contracts found in the workspace."
|
|
25
|
+
|
|
26
|
+
lines = ["# Detected Contracts\n"]
|
|
27
|
+
for c in contracts:
|
|
28
|
+
lines.append(f"## {c['title']} (v{c['version']})")
|
|
29
|
+
lines.append(f"- File: {c['file']}")
|
|
30
|
+
if c["schemas"]:
|
|
31
|
+
lines.append(f"- Schemas: {', '.join(c['schemas'][:10])}")
|
|
32
|
+
if c["paths"]:
|
|
33
|
+
lines.append(f"- Paths: {', '.join(c['paths'][:8])}")
|
|
34
|
+
lines.append("")
|
|
35
|
+
return "\n".join(lines)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
def trace_contract(contract_name: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Trace a specific contract by name (schema or title).
|
|
42
|
+
Shows where it is defined and basic details.
|
|
43
|
+
Example: trace_contract("Order")
|
|
44
|
+
"""
|
|
45
|
+
contracts = extract_contracts(WORKSPACE)
|
|
46
|
+
matches = []
|
|
47
|
+
|
|
48
|
+
for c in contracts:
|
|
49
|
+
if (contract_name.lower() in c["title"].lower() or
|
|
50
|
+
contract_name.lower() in [s.lower() for s in c["schemas"]] or
|
|
51
|
+
any(contract_name.lower() in p.lower() for p in c["paths"])):
|
|
52
|
+
matches.append(c)
|
|
53
|
+
|
|
54
|
+
if not matches:
|
|
55
|
+
return f"No contract found matching '{contract_name}'."
|
|
56
|
+
|
|
57
|
+
lines = [f"# Trace: {contract_name}\n"]
|
|
58
|
+
for c in matches:
|
|
59
|
+
lines.append(f"**Defined in:** {c['file']}")
|
|
60
|
+
lines.append(f"**Title:** {c['title']} (v{c['version']})")
|
|
61
|
+
if c["schemas"]:
|
|
62
|
+
lines.append(f"**Schemas:** {', '.join(c['schemas'])}")
|
|
63
|
+
if c["paths"]:
|
|
64
|
+
lines.append(f"**Paths:** {', '.join(c['paths'])}")
|
|
65
|
+
lines.append("")
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
from mesh_mcp.detectors.python_detector import find_python_consumers
|
|
70
|
+
|
|
71
|
+
@mcp.tool()
|
|
72
|
+
def find_consumers(schema_name: str) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Find Python files that appear to consume a given schema.
|
|
75
|
+
Example: find_consumers("Order")
|
|
76
|
+
"""
|
|
77
|
+
contracts = extract_contracts(WORKSPACE)
|
|
78
|
+
all_schemas = []
|
|
79
|
+
for c in contracts:
|
|
80
|
+
all_schemas.extend(c["schemas"])
|
|
81
|
+
|
|
82
|
+
if schema_name not in all_schemas and not any(schema_name.lower() in s.lower() for s in all_schemas):
|
|
83
|
+
return f"Schema '{schema_name}' not found in any OpenAPI contract."
|
|
84
|
+
|
|
85
|
+
consumers = find_python_consumers(WORKSPACE, [schema_name])
|
|
86
|
+
|
|
87
|
+
if not consumers:
|
|
88
|
+
return f"No Python consumers found for '{schema_name}'."
|
|
89
|
+
|
|
90
|
+
lines = [f"# Consumers of {schema_name}\n"]
|
|
91
|
+
for cons in consumers:
|
|
92
|
+
lines.append(f"- {cons['file']} ({cons['reason']})")
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def check_blast_radius(schema_name: str, change_description: str = "") -> str:
|
|
98
|
+
"""
|
|
99
|
+
Check the blast radius of changing a schema.
|
|
100
|
+
Shows which files may be affected.
|
|
101
|
+
Example: check_blast_radius("Order", "removed field tax_amount")
|
|
102
|
+
"""
|
|
103
|
+
contracts = extract_contracts(WORKSPACE)
|
|
104
|
+
matching = [c for c in contracts if schema_name in c["schemas"] or schema_name.lower() in c["title"].lower()]
|
|
105
|
+
|
|
106
|
+
if not matching:
|
|
107
|
+
return f"Schema '{schema_name}' not found."
|
|
108
|
+
|
|
109
|
+
consumers = find_python_consumers(WORKSPACE, [schema_name])
|
|
110
|
+
|
|
111
|
+
lines = [f"# Blast Radius: {schema_name}\n"]
|
|
112
|
+
if change_description:
|
|
113
|
+
lines.append(f"**Proposed change:** {change_description}\n")
|
|
114
|
+
|
|
115
|
+
lines.append("**Defined in:**")
|
|
116
|
+
for c in matching:
|
|
117
|
+
lines.append(f"- {c['file']} ({c['title']})")
|
|
118
|
+
|
|
119
|
+
lines.append("\n**Potentially affected consumers:**")
|
|
120
|
+
if consumers:
|
|
121
|
+
for cons in consumers:
|
|
122
|
+
# Skip the server file itself for cleaner output
|
|
123
|
+
if "server.py" in cons["file"]:
|
|
124
|
+
continue
|
|
125
|
+
lines.append(f"- {cons['file']} — {cons['reason']}")
|
|
126
|
+
else:
|
|
127
|
+
lines.append("- No consumers detected")
|
|
128
|
+
|
|
129
|
+
lines.append("\n**Recommendation:** Review the listed files before making breaking changes.")
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
mcp.run(transport="stdio")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for mesh-mcp."""
|