Spaces:
Running
Running
| import json | |
| import gradio as gr | |
| import requests | |
| import base64 | |
| import markdown | |
| GITHUB = "https://api.github.com" | |
| # --------------------------------------------------------- | |
| # MCP RESOURCES (HTML Renderers) | |
| # --------------------------------------------------------- | |
| def repo_info_ui(): | |
| return """ | |
| <div id="repo"></div> | |
| <script> | |
| function render() { | |
| let d; | |
| try { | |
| // Parse the JSON string | |
| d = JSON.parse(window.openai?.toolOutput?.text || '{}'); | |
| } catch (e) { | |
| return; | |
| } | |
| if (!d.full_name){ | |
| document.getElementById("repo").innerHTML = "<p>No repository info found.</p>"; | |
| return; | |
| } | |
| document.getElementById("repo").innerHTML = ` | |
| <h2>${d.full_name}</h2> | |
| <p>${d.description || "No description"}</p> | |
| <div style="display:flex;gap:1rem;margin-top:1rem;font-size:15px"> | |
| <span>⭐ ${d.stars}</span> | |
| <span>🍴 ${d.forks}</span> | |
| <span>❗ ${d.open_issues}</span> | |
| </div> | |
| <p style="margin-top:1rem;"> | |
| <a href="${d.html_url}" target="_blank">Open on GitHub</a> | |
| </p> | |
| `; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", render); | |
| </script> | |
| """ | |
| def issues_ui(): | |
| return """ | |
| <div id="issues"></div> | |
| <script> | |
| function render() { | |
| let issues; | |
| try { | |
| // Parse the JSON string | |
| issues = JSON.parse(window.openai?.toolOutput?.text || '[]'); | |
| } catch (e) { | |
| return; | |
| } | |
| let html = "<h3>Issues</h3><ul>"; | |
| for (const i of issues) { | |
| html += `<li><a href="${i.url}" target="_blank">#${i.number} — ${i.title}</a></li>`; | |
| } | |
| html += "</ul>"; | |
| document.getElementById("issues").innerHTML = html; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", (event) => { | |
| if (event.detail?.globals?.toolInput) { | |
| render(); | |
| } | |
| }, { passive: true }); | |
| </script> | |
| """ | |
| def pulls_ui(): | |
| return """ | |
| <div id="pulls"></div> | |
| <script> | |
| function render() { | |
| const toolOutput = window.openai?.toolOutput; | |
| console.log("toolOutput:", window.openai?.toolOutput); | |
| if (!toolOutput || !toolOutput.text) { | |
| document.getElementById("pulls").innerHTML = "<p>No PRs found.</p>"; | |
| return; | |
| } | |
| const jsonString = toolOutput.text | |
| let prs = []; | |
| try { | |
| prs = JSON.parse(jsonString); | |
| } catch (e) { | |
| console.error("Failed to parse PR list:", e); | |
| } | |
| let html = "<h3>Pull Requests</h3><ul>"; | |
| for (const p of prs) { | |
| html += `<li><a href="${p.url}" target="_blank">#${p.number} — ${p.title}</a></li>`; | |
| } | |
| html += "</ul>"; | |
| document.getElementById("pulls").innerHTML = html; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", (event) => { | |
| console.log("Received openai:set_globals event:", event); | |
| render(); | |
| }); | |
| </script> | |
| """ | |
| def readme_ui(): | |
| return """ | |
| <div id="readme"></div> | |
| <script> | |
| function render() { | |
| d = JSON.parse(window.openai?.toolOutput?.text || '{}'); | |
| if(d?.html === undefined) { | |
| document.getElementById("readme").innerHTML = "<p>No README found.</p>"; | |
| return; | |
| } | |
| const html = d.html; | |
| document.getElementById("readme").innerHTML = `<h2>README</h2><div style="padding:1rem;background:#fafafa;border:1px solid #ccc">${html}</div>`; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", render); | |
| </script> | |
| """ | |
| def contributors_ui(): | |
| return """ | |
| <div id="contributors"></div> | |
| <script> | |
| function render() { | |
| let list; | |
| try { | |
| // Parse the JSON string | |
| list = JSON.parse(window.openai?.toolOutput?.text || '[]'); | |
| } catch (e) { | |
| return; | |
| } | |
| let html = "<h3>Top Contributors</h3><ul>"; | |
| for (const c of list) { | |
| html += `<li><img src="${c.avatar}" width="24" style="border-radius:50%;vertical-align:middle;margin-right:6px"><a href="${c.url}" target="_blank">${c.name}</a> — ${c.commits} commits</li>`; | |
| } | |
| html += "</ul>"; | |
| document.getElementById("contributors").innerHTML = html; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", render); | |
| </script> | |
| """ | |
| def search_ui(): | |
| return """ | |
| <div id="repos"></div> | |
| <script> | |
| function render() { | |
| let list; | |
| try { | |
| // Parse the JSON string | |
| list = JSON.parse(window.openai?.toolOutput?.text || '[]'); | |
| } catch (e) { | |
| return; | |
| } | |
| let html = "<h3>Search Results</h3><ul>"; | |
| for (const r of list) { | |
| html += `<li><a href="${r.url}" target="_blank">${r.full_name}</a> — ⭐ ${r.stars}<br/><small>${r.description || ""}</small></li>`; | |
| } | |
| html += "</ul>"; | |
| document.getElementById("repos").innerHTML = html; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", render); | |
| </script> | |
| """ | |
| def files_ui(): | |
| return """ | |
| <div id="files"></div> | |
| <script> | |
| function render() { | |
| let items; | |
| try { | |
| // Parse the JSON string | |
| items = JSON.parse(window.openai?.toolOutput?.text || '[]'); | |
| } catch (e) { | |
| return; | |
| } | |
| let html = "<h3>Files</h3><ul>"; | |
| for (const f of items) { | |
| html += `<li>${f.type === "dir" ? "📁" : "📄"} <a href="${f.url}" target="_blank">${f.path}</a></li>`; | |
| } | |
| html += "</ul>"; | |
| document.getElementById("files").innerHTML = html; | |
| } | |
| render(); | |
| window.addEventListener("openai:set_globals", render); | |
| </script> | |
| """ | |
| # --------------------------------------------------------- | |
| # HELPERS | |
| # --------------------------------------------------------- | |
| def gget(url, params=None): | |
| r = requests.get(url, params=params, headers={"Accept": "application/vnd.github+json"}) | |
| if r.status_code != 200: | |
| raise Exception(f"GitHub API error ({r.status_code}): {r.text}") | |
| return r.json() | |
| # --------------------------------------------------------- | |
| # MCP TOOLS (with widget metadata) | |
| # --------------------------------------------------------- | |
| def tool_meta(resource_ui): | |
| """Helper to generate the full _meta dict for tools""" | |
| return { | |
| "openai/outputTemplate": resource_ui, | |
| "openai/resultCanProduceWidget": True, | |
| "openai/widgetAccessible": True | |
| } | |
| def get_repo_info(full_name: str) -> str: | |
| d = gget(f"{GITHUB}/repos/{full_name}") | |
| info = { | |
| "full_name": d["full_name"], | |
| "description": d.get("description", ""), | |
| "stars": d["stargazers_count"], | |
| "forks": d["forks_count"], | |
| "open_issues": d["open_issues_count"], | |
| "html_url": d["html_url"] | |
| } | |
| return json.dumps(info) | |
| def list_issues(full_name: str, per_page: int = 10) -> str: | |
| issues = gget(f"{GITHUB}/repos/{full_name}/issues", {"per_page": per_page}) | |
| issues = [{"title": i["title"], "number": i["number"], "url": i["html_url"]} for i in issues] | |
| return json.dumps(issues) | |
| def list_pull_requests(full_name: str, per_page: int = 10) -> str: | |
| prs = gget(f"{GITHUB}/repos/{full_name}/pulls", {"per_page": per_page}) | |
| prs = [{"title": p["title"], "number": p["number"], "url": p["html_url"]} for p in prs] | |
| return json.dumps(prs) | |
| def get_readme(full_name: str) -> str: | |
| d = gget(f"{GITHUB}/repos/{full_name}/readme") | |
| content = base64.b64decode(d["content"]).decode() | |
| html = markdown.markdown(content) | |
| data = {"html": html} | |
| return json.dumps(data) | |
| def list_contributors(full_name: str, limit: int = 10) -> str: | |
| c = gget(f"{GITHUB}/repos/{full_name}/contributors") | |
| c = [{ | |
| "name": i["login"], | |
| "avatar": i["avatar_url"], | |
| "url": i["html_url"], | |
| "commits": i["contributions"] | |
| } for i in c[:limit]] | |
| return json.dumps(c) | |
| def search_repos(query: str, language: str = None, per_page: int = 10) -> str: | |
| params = {"q": query, "per_page": per_page, "sort": "stars"} | |
| if language: | |
| params["q"] += f"+language:{language}" | |
| d = gget(f"{GITHUB}/search/repositories", params) | |
| d = [{"full_name": r["full_name"], "stars": r["stargazers_count"], "description": r["description"], "url": r["html_url"]} for r in d["items"]] | |
| return json.dumps(d) | |
| def list_files(full_name: str) -> str: | |
| d = gget(f"{GITHUB}/repos/{full_name}/git/trees/HEAD?recursive=1") | |
| d = [{"path": i["path"], "type": i["type"], "url": f"https://github.com/{full_name}/blob/HEAD/{i['path']}"} for i in d["tree"]] | |
| return json.dumps(d) | |
| # --------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("### GitHub Helper Bot — use inside ChatGPT") | |
| repo_input = gr.Textbox(label="Repository Full Name (owner/repo)") | |
| btn_repo = gr.Button("Get Repo Info") | |
| btn_issues = gr.Button("List Issues") | |
| btn_prs = gr.Button("List Pull Requests") | |
| btn_readme = gr.Button("Get README") | |
| btn_contrib = gr.Button("List Contributors") | |
| btn_files = gr.Button("List Files") | |
| search_query = gr.Textbox(label="Search Repositories") | |
| btn_search = gr.Button("Search Repos") | |
| tool_response = gr.TextArea(label="Tool Response") | |
| html_out = gr.TextArea(label="HTML Output") | |
| # Wire tools + resources | |
| btn_repo.click(get_repo_info, inputs=repo_input, outputs=tool_response) | |
| btn_repo.click(repo_info_ui, outputs=html_out) | |
| btn_issues.click(list_issues, inputs=repo_input, outputs=tool_response) | |
| btn_issues.click(issues_ui, outputs=html_out) | |
| btn_prs.click(list_pull_requests, inputs=repo_input, outputs=tool_response) | |
| btn_prs.click(pulls_ui, outputs=html_out) | |
| btn_readme.click(get_readme, inputs=repo_input, outputs=tool_response) | |
| btn_readme.click(readme_ui, outputs=html_out) | |
| btn_contrib.click(list_contributors, inputs=repo_input, outputs=tool_response) | |
| btn_contrib.click(contributors_ui, outputs=html_out) | |
| btn_files.click(list_files, inputs=repo_input, outputs=tool_response) | |
| btn_files.click(files_ui, outputs=html_out) | |
| btn_search.click(search_repos, inputs=search_query, outputs=tool_response) | |
| btn_search.click(search_ui, outputs=html_out) | |
| if __name__ == "__main__": | |
| demo.launch(share=True, mcp_server=True) |