Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/build-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,42 @@ jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Check Docker credentials
id: docker_creds
shell: bash
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
if [ -n "$DOCKERHUB_USERNAME" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "Docker publish skipped: missing DOCKERHUB_USERNAME/DOCKERHUB_TOKEN secrets." >> "$GITHUB_STEP_SUMMARY"
fi
-
name: Checkout
if: steps.docker_creds.outputs.enabled == 'true'
uses: actions/checkout@v3
-
name: Set up QEMU
if: steps.docker_creds.outputs.enabled == 'true'
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
if: steps.docker_creds.outputs.enabled == 'true'
uses: docker/setup-buildx-action@v3
-
name: Login to Docker Hub
if: steps.docker_creds.outputs.enabled == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push Dockerfile
if: steps.docker_creds.outputs.enabled == 'true'
uses: docker/build-push-action@v5
with:
file: ./Docker/Dockerfile
Expand All @@ -55,6 +74,7 @@ jobs:

-
name: Build and push Dockerfile mac
if: steps.docker_creds.outputs.enabled == 'true'
uses: docker/build-push-action@v5
with:
file: ./Docker/Dockerfile_mac
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/deploy-ecosystem.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,42 @@ jobs:
matrix:
project: ${{ fromJson(needs.determine-projects.outputs.projects_json) }}
steps:
- name: Check Vercel token
id: vercel_token
shell: bash
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
if [ -n "$VERCEL_TOKEN" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "Vercel deploy skipped: missing VERCEL_TOKEN secret." >> "$GITHUB_STEP_SUMMARY"
fi

- name: Checkout repository
if: steps.vercel_token.outputs.enabled == 'true'
uses: actions/checkout@v4

- name: Setup Node.js
if: steps.vercel_token.outputs.enabled == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install Vercel CLI
if: steps.vercel_token.outputs.enabled == 'true'
run: npm install --global vercel@latest

- name: Configure Git for Vercel
if: steps.vercel_token.outputs.enabled == 'true'
working-directory: ${{ matrix.project.path }}
run: |
git config user.name "github-actions[bot]"
git config user.email "support@resilientdb.com"

- name: Pull Vercel Environment Information
if: steps.vercel_token.outputs.enabled == 'true'
working-directory: ${{ matrix.project.path }}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand All @@ -163,6 +181,7 @@ jobs:
vercel pull --yes --environment=production --token=$VERCEL_TOKEN || echo "Could not pull Vercel config, continuing..."

- name: Create deployment commit with correct author
if: steps.vercel_token.outputs.enabled == 'true'
run: |
# Configure git user
git config user.name "github-actions[bot]"
Expand All @@ -171,6 +190,7 @@ jobs:
git commit --allow-empty -m "ci: authorize deploy for ${{ matrix.project.name }}" || echo "Skipping commit creation"

- name: Deploy to Vercel
if: steps.vercel_token.outputs.enabled == 'true'
working-directory: ${{ matrix.project.path }}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down
171 changes: 171 additions & 0 deletions ecosystem/ai-tools/mcp/ResInsight/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,177 @@ async def show_dependency_graph(owner:str, repo:str, branch:str="main") -> dict:
# Return base64 PNG string so clients can display
return {"image_base64": img_data}


def parse_python_imports_v2(file_content: str) -> List[str]:
"""Parse Python imports using AST with a fallback line parser."""
imports: List[str] = []

try:
tree = ast.parse(file_content)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name:
imports.append(alias.name)
elif isinstance(node, ast.ImportFrom):
if node.module:
imports.append(node.module)
except Exception:
for raw_line in file_content.splitlines():
line = raw_line.strip()
if line.startswith("import "):
rhs = line[len("import "):]
for part in rhs.split(","):
mod = part.strip().split(" as ")[0].strip()
if mod:
imports.append(mod)
elif line.startswith("from ") and " import " in line:
mod = line[len("from "):].split(" import ", 1)[0].strip()
if mod:
imports.append(mod)

deduped: List[str] = []
seen = set()
for name in imports:
if name not in seen:
seen.add(name)
deduped.append(name)
return deduped


async def analyze_imports_v2(
owner: str,
repo: str,
branch: str = "main",
max_python_files: int = 50,
) -> Dict[str, Any]:
"""Build a repo-backed dependency graph plus provenance metadata."""
try:
files_meta = await fetch_repo_tree(owner, repo, branch)
except Exception as e:
return {"error": f"Could not fetch repository tree: {str(e)}"}

if not files_meta:
return {"error": "Repository tree is empty or unavailable."}

python_files = [f.get("path", "") for f in files_meta if f.get("path", "").endswith(".py")]
selected_files = python_files[:max_python_files]

g = nx.DiGraph()
edges_preview: List[Dict[str, str]] = []
processed_files = 0

for filepath in selected_files:
content = await fetch_raw_file(owner, repo, filepath, branch)
if not content:
continue

processed_files += 1
g.add_node(filepath)

imports = parse_python_imports_v2(content)
for imp in imports:
g.add_edge(filepath, imp)
if len(edges_preview) < 200:
edges_preview.append({"source": filepath, "target": imp})

if g.number_of_nodes() == 0:
return {
"error": "No analyzable Python files found.",
"provenance": {
"owner": owner,
"repo": repo,
"branch": branch,
"total_repo_python_files": len(python_files),
"selected_files": selected_files,
"processed_files": 0,
},
}

pos = nx.spring_layout(g, k=0.55, iterations=80, seed=42)
plt.figure(figsize=(16, 12))
nx.draw_networkx(
g,
pos=pos,
with_labels=True,
font_size=9,
node_size=650,
node_color="#BFE8D6",
edge_color="#666666",
arrowsize=14,
arrowstyle="->",
)
plt.axis("off")
plt.tight_layout()

buf = io.BytesIO()
plt.savefig(buf, format="png")
buf.seek(0)
image_base64 = base64.b64encode(buf.read()).decode("utf-8")
plt.close()

return {
"image_base64": image_base64,
"provenance": {
"owner": owner,
"repo": repo,
"branch": branch,
"source": "github_api_only",
"total_repo_python_files": len(python_files),
"selected_files": selected_files,
"processed_files": processed_files,
"selected_file_limit": max_python_files,
"graph_nodes": g.number_of_nodes(),
"graph_edges": g.number_of_edges(),
"edges_preview": edges_preview,
},
}


@mcp.tool(name="ShowDependencyGraphV2")
async def show_dependency_graph_v2(
owner: str,
repo: str,
branch: str = "main",
max_python_files: int = 50,
) -> dict:
"""
Trusted graph generation that is strictly built from repository file content
fetched through this MCP server. Includes provenance metadata for auditability.
"""
result = await analyze_imports_v2(owner, repo, branch, max_python_files=max_python_files)
if "error" in result:
return result
return result


@mcp.tool(name="KGraphQueryV2")
async def kgraph_query_v2(
owner: str,
repo: str,
node_name: str,
branch: str = "main",
max_python_files: int = 50,
) -> dict:
"""
Query neighbors for a node from a graph built from current repository content.
"""
result = await analyze_imports_v2(owner, repo, branch, max_python_files=max_python_files)
if "error" in result:
return result

edges_preview = result.get("provenance", {}).get("edges_preview", [])
outgoing = [e["target"] for e in edges_preview if e.get("source") == node_name]
incoming = [e["source"] for e in edges_preview if e.get("target") == node_name]

return {
"node": node_name,
"outgoing_neighbors": outgoing,
"incoming_neighbors": incoming,
"note": "Neighbors are computed from the previewed subset of graph edges.",
"provenance": result.get("provenance", {}),
}

def get_file_type(filepath: str) -> str:
"""Helper to identify file type by extension."""
ext_map = {
Expand Down
Loading