Improve MCP tool calling and routing
Add explicit JSON tool-call protocol for local providers, improve parsing of JSON-only tool calls, and add heuristic routing to MCP-capable profiles for repo/PR intents. Also document and mount local-cloned MCP servers and expand MCP env var handling. Made-with: Cursor
This commit is contained in:
parent
93b34bc214
commit
7050e032e8
5
.gitignore
vendored
5
.gitignore
vendored
@ -22,3 +22,8 @@ poetry.lock
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
botpy.log
|
botpy.log
|
||||||
tests/
|
tests/
|
||||||
|
|
||||||
|
# Local-cloned MCP servers (kept out of git; clone/build locally)
|
||||||
|
mcp-servers/*
|
||||||
|
!mcp-servers/README.md
|
||||||
|
!mcp-servers/.gitkeep
|
||||||
|
|||||||
@ -18,6 +18,17 @@ cat > ~/.nanobot-user1/config.json << 'EOF'
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"allowFrom": ["adayear2025@gmail.com"]
|
"allowFrom": ["adayear2025@gmail.com"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "/app/mcp-servers/gitea-mcp/gitea-mcp",
|
||||||
|
"args": ["-t", "stdio", "--host", "http://10.0.30.169:3000", "-r"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_ACCESS_TOKEN": "$NANOBOT_GITLE_TOKEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@ -19,6 +19,8 @@ services:
|
|||||||
- ~/.nanobot/workspaces/ilia:/workspace
|
- ~/.nanobot/workspaces/ilia:/workspace
|
||||||
# Mount source code for development (changes picked up immediately)
|
# Mount source code for development (changes picked up immediately)
|
||||||
- ./nanobot:/app/nanobot:ro # Read-only mount (safer)
|
- ./nanobot:/app/nanobot:ro # Read-only mount (safer)
|
||||||
|
# Local-cloned MCP servers (see scripts/setup-mcp-servers.sh)
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
# Or use this for read-write (if you edit inside container):
|
# Or use this for read-write (if you edit inside container):
|
||||||
# - ./nanobot:/app/nanobot
|
# - ./nanobot:/app/nanobot
|
||||||
ports:
|
ports:
|
||||||
@ -46,6 +48,7 @@ services:
|
|||||||
- ~/.nanobot-user2:/root/.nanobot
|
- ~/.nanobot-user2:/root/.nanobot
|
||||||
- ~/.nanobot/workspaces/family:/workspace
|
- ~/.nanobot/workspaces/family:/workspace
|
||||||
- ./nanobot:/app/nanobot:ro
|
- ./nanobot:/app/nanobot:ro
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18791:18790"
|
- "18791:18790"
|
||||||
deploy:
|
deploy:
|
||||||
@ -71,6 +74,7 @@ services:
|
|||||||
- ~/.nanobot-user3:/root/.nanobot
|
- ~/.nanobot-user3:/root/.nanobot
|
||||||
- ~/.nanobot/workspaces/wife:/workspace
|
- ~/.nanobot/workspaces/wife:/workspace
|
||||||
- ./nanobot:/app/nanobot:ro
|
- ./nanobot:/app/nanobot:ro
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18792:18790"
|
- "18792:18790"
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@ -18,6 +18,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ~/.nanobot-user1:/root/.nanobot
|
- ~/.nanobot-user1:/root/.nanobot
|
||||||
- ~/.nanobot/workspaces/ilia:/workspace
|
- ~/.nanobot/workspaces/ilia:/workspace
|
||||||
|
# Local-cloned MCP servers (see scripts/setup-mcp-servers.sh)
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18790:18790"
|
- "18790:18790"
|
||||||
deploy:
|
deploy:
|
||||||
@ -42,6 +44,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ~/.nanobot-user2:/root/.nanobot
|
- ~/.nanobot-user2:/root/.nanobot
|
||||||
- ~/.nanobot/workspaces/family:/workspace
|
- ~/.nanobot/workspaces/family:/workspace
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18791:18790"
|
- "18791:18790"
|
||||||
deploy:
|
deploy:
|
||||||
@ -66,6 +69,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ~/.nanobot-user3:/root/.nanobot
|
- ~/.nanobot-user3:/root/.nanobot
|
||||||
- ~/.nanobot/workspaces/wife:/workspace
|
- ~/.nanobot/workspaces/wife:/workspace
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18792:18790"
|
- "18792:18790"
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
@ -16,6 +16,8 @@ services:
|
|||||||
- ~/.nanobot-user1:/root/.nanobot
|
- ~/.nanobot-user1:/root/.nanobot
|
||||||
# @ilia — isolated workspace + memory (host: ~/.nanobot/workspaces/ilia)
|
# @ilia — isolated workspace + memory (host: ~/.nanobot/workspaces/ilia)
|
||||||
- ~/.nanobot/workspaces/ilia:/workspace
|
- ~/.nanobot/workspaces/ilia:/workspace
|
||||||
|
# Local-cloned MCP servers (see scripts/setup-mcp-servers.sh)
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18790:18790"
|
- "18790:18790"
|
||||||
deploy:
|
deploy:
|
||||||
@ -41,6 +43,7 @@ services:
|
|||||||
- ~/.nanobot-user2:/root/.nanobot
|
- ~/.nanobot-user2:/root/.nanobot
|
||||||
# @family — isolated workspace + memory
|
# @family — isolated workspace + memory
|
||||||
- ~/.nanobot/workspaces/family:/workspace
|
- ~/.nanobot/workspaces/family:/workspace
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18791:18790"
|
- "18791:18790"
|
||||||
deploy:
|
deploy:
|
||||||
@ -66,6 +69,7 @@ services:
|
|||||||
- ~/.nanobot-user3:/root/.nanobot
|
- ~/.nanobot-user3:/root/.nanobot
|
||||||
# @wife — isolated workspace + memory
|
# @wife — isolated workspace + memory
|
||||||
- ~/.nanobot/workspaces/wife:/workspace
|
- ~/.nanobot/workspaces/wife:/workspace
|
||||||
|
- ./mcp-servers:/app/mcp-servers:ro
|
||||||
ports:
|
ports:
|
||||||
- "18792:18790"
|
- "18792:18790"
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
1
mcp-servers/.gitkeep
Normal file
1
mcp-servers/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
34
mcp-servers/README.md
Normal file
34
mcp-servers/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Local MCP servers
|
||||||
|
|
||||||
|
This repo uses a **local-clone policy** for MCP servers: clone upstream repos into `./mcp-servers/` and run them from disk (instead of fetching from npm/PyPI at runtime).
|
||||||
|
|
||||||
|
## Gitea MCP
|
||||||
|
|
||||||
|
- **Upstream**: `https://gitea.com/gitea/gitea-mcp.git`
|
||||||
|
- **Local path**: `mcp-servers/gitea-mcp/`
|
||||||
|
- **Binary**: `mcp-servers/gitea-mcp/gitea-mcp`
|
||||||
|
|
||||||
|
Build it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/setup-mcp-servers.sh gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
Then configure nanobot (example):
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"mcpServers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "./mcp-servers/gitea-mcp/gitea-mcp",
|
||||||
|
"args": ["-t", "stdio", "--host", "http://10.0.30.169:3000"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_ACCESS_TOKEN": "$NANOBOT_GITLE_TOKEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@ -89,6 +89,22 @@ You are nanobot, a helpful AI assistant. You have access to tools that allow you
|
|||||||
- Send messages to users on chat channels
|
- Send messages to users on chat channels
|
||||||
- Spawn subagents for complex background tasks
|
- Spawn subagents for complex background tasks
|
||||||
|
|
||||||
|
## Tool calling (IMPORTANT)
|
||||||
|
Some LLM backends may not support native function-calling. When you decide to use a tool, you MUST output a single JSON object in one of these formats (and no other surrounding text):
|
||||||
|
|
||||||
|
1) Standard tool call:
|
||||||
|
{{"name":"<tool_name>","parameters":{{...}}}}
|
||||||
|
|
||||||
|
2) Calendar shortcut (allowed only for the built-in `calendar` tool):
|
||||||
|
{{"action":"list_events", ...}}
|
||||||
|
|
||||||
|
After a tool result is returned, respond normally in plain text unless the user asks for another tool action.
|
||||||
|
|
||||||
|
### MCP quick mappings (use these when the intent matches)
|
||||||
|
- If the user asks for **my Gitea user info** (who am I / my profile / my account): call `mcp_gitea_get_me` with `{{}}`.
|
||||||
|
- If the user asks for **Gitea MCP server version**: call `mcp_gitea_get_gitea_mcp_server_version` with `{{}}`.
|
||||||
|
- If the user asks to **list my repos**: call `mcp_gitea_list_my_repos` with pagination defaults.
|
||||||
|
|
||||||
## Current Time
|
## Current Time
|
||||||
{now} ({tz})
|
{now} ({tz})
|
||||||
|
|
||||||
|
|||||||
@ -311,6 +311,7 @@ class AgentLoop:
|
|||||||
iteration = 0
|
iteration = 0
|
||||||
final_content = None
|
final_content = None
|
||||||
tools_used: list[str] = []
|
tools_used: list[str] = []
|
||||||
|
empty_final_retry_used = False
|
||||||
|
|
||||||
from nanobot.agent.tool_profiles import (
|
from nanobot.agent.tool_profiles import (
|
||||||
compute_allowed_tool_names,
|
compute_allowed_tool_names,
|
||||||
@ -418,6 +419,24 @@ class AgentLoop:
|
|||||||
else:
|
else:
|
||||||
final_content = self._strip_think(response.content)
|
final_content = self._strip_think(response.content)
|
||||||
logger.info(f"Final response generated. Content length: {len(final_content) if final_content else 0}")
|
logger.info(f"Final response generated. Content length: {len(final_content) if final_content else 0}")
|
||||||
|
# Some local OpenAI-compatible backends occasionally return an empty assistant message.
|
||||||
|
# Retry once with an explicit nudge to either call a tool or answer in text.
|
||||||
|
if (not final_content or not final_content.strip()) and not empty_final_retry_used:
|
||||||
|
empty_final_retry_used = True
|
||||||
|
logger.warning(
|
||||||
|
"LLM returned empty final content; retrying once with a non-empty response nudge"
|
||||||
|
)
|
||||||
|
messages = messages + [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
"Your previous reply was empty. You MUST either (a) call an appropriate tool, "
|
||||||
|
"or (b) respond with a short helpful text answer. Do not return an empty message."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
final_content = None
|
||||||
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
if final_content is None and iteration >= self.max_iterations:
|
if final_content is None and iteration >= self.max_iterations:
|
||||||
|
|||||||
@ -27,6 +27,42 @@ async def route_tool_profile(
|
|||||||
if not profiles:
|
if not profiles:
|
||||||
return default_profile
|
return default_profile
|
||||||
|
|
||||||
|
# Heuristic fast-path: if the request clearly needs a dev/forge MCP (PRs, issues, repos),
|
||||||
|
# prefer an MCP-enabled profile without spending an LLM call.
|
||||||
|
msg_l = (user_message or "").lower()
|
||||||
|
needs_forge = any(
|
||||||
|
k in msg_l
|
||||||
|
for k in [
|
||||||
|
"pull request",
|
||||||
|
"pull requests",
|
||||||
|
"open pr",
|
||||||
|
"open prs",
|
||||||
|
" list prs",
|
||||||
|
"pr ",
|
||||||
|
"prs",
|
||||||
|
"merge request",
|
||||||
|
"issue",
|
||||||
|
"issues",
|
||||||
|
"gitea",
|
||||||
|
"repo",
|
||||||
|
"repository",
|
||||||
|
"branches",
|
||||||
|
"commits",
|
||||||
|
"tags",
|
||||||
|
"release",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if needs_forge:
|
||||||
|
# Prefer an explicit "*mcp*" profile key if present, else any profile that enables MCP servers.
|
||||||
|
for key in profiles.keys():
|
||||||
|
if "mcp" in key.lower():
|
||||||
|
logger.info(f"Tool router selected profile '{key}' (heuristic)")
|
||||||
|
return key
|
||||||
|
for key, p in profiles.items():
|
||||||
|
if p.mcp_servers is None or (isinstance(p.mcp_servers, list) and len(p.mcp_servers) > 0):
|
||||||
|
logger.info(f"Tool router selected profile '{key}' (heuristic)")
|
||||||
|
return key
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for name, p in profiles.items():
|
for name, p in profiles.items():
|
||||||
desc = (p.description or "").strip() or "(no description)"
|
desc = (p.description or "").strip() or "(no description)"
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -118,9 +119,26 @@ async def connect_mcp_server(
|
|||||||
from mcp import ClientSession, StdioServerParameters
|
from mcp import ClientSession, StdioServerParameters
|
||||||
from mcp.client.stdio import stdio_client
|
from mcp.client.stdio import stdio_client
|
||||||
|
|
||||||
|
def _expand_env(env: dict[str, str]) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Expand $VARS in cfg.env using the current process environment.
|
||||||
|
|
||||||
|
This lets configs safely reference secrets that are already injected into the
|
||||||
|
container environment (e.g. via .env.shared), without duplicating them in JSON:
|
||||||
|
{ "GITEA_ACCESS_TOKEN": "$NANOBOT_GITLE_TOKEN" }
|
||||||
|
"""
|
||||||
|
if not env:
|
||||||
|
return {}
|
||||||
|
expanded: dict[str, str] = {}
|
||||||
|
for k, v in env.items():
|
||||||
|
if v is None:
|
||||||
|
continue
|
||||||
|
expanded[k] = os.path.expandvars(str(v))
|
||||||
|
return expanded
|
||||||
|
|
||||||
if cfg.command:
|
if cfg.command:
|
||||||
params = StdioServerParameters(
|
params = StdioServerParameters(
|
||||||
command=cfg.command, args=cfg.args, env=cfg.env or None
|
command=cfg.command, args=cfg.args, env=_expand_env(cfg.env) or None
|
||||||
)
|
)
|
||||||
read, write = await stack.enter_async_context(stdio_client(params))
|
read, write = await stack.enter_async_context(stdio_client(params))
|
||||||
elif cfg.url:
|
elif cfg.url:
|
||||||
|
|||||||
@ -59,15 +59,38 @@ class CustomProvider(LLMProvider):
|
|||||||
for tc in (msg.tool_calls or [])
|
for tc in (msg.tool_calls or [])
|
||||||
]
|
]
|
||||||
|
|
||||||
# If no structured tool calls, try to parse from content (Ollama sometimes returns JSON in content)
|
# If no structured tool calls, try to parse from content (some OpenAI-compatible backends return JSON in content)
|
||||||
# Only parse if content looks like it contains a tool call JSON (to avoid false positives)
|
# Only parse if content looks like it contains a tool call JSON (to avoid false positives)
|
||||||
content = msg.content or ""
|
content = msg.content or ""
|
||||||
|
stripped = content.strip()
|
||||||
|
# Note: This list should match tools registered in AgentLoop._register_default_tools().
|
||||||
|
# MCP tools are registered dynamically and are prefixed with "mcp_" (allow those too).
|
||||||
|
valid_tools = [
|
||||||
|
# File tools
|
||||||
|
"read_file", "write_file", "edit_file", "list_dir",
|
||||||
|
# Shell tool
|
||||||
|
"exec",
|
||||||
|
# Web tools
|
||||||
|
"web_search", "web_fetch",
|
||||||
|
# Communication tools
|
||||||
|
"message", "spawn",
|
||||||
|
# Calendar tool
|
||||||
|
"calendar",
|
||||||
|
# Cron tool
|
||||||
|
"cron",
|
||||||
|
# Email tool
|
||||||
|
"email",
|
||||||
|
]
|
||||||
# Check for standard format: {"name": "...", "parameters": {...}}
|
# Check for standard format: {"name": "...", "parameters": {...}}
|
||||||
has_standard_format = '"name"' in content and '"parameters"' in content
|
has_standard_format = '"name"' in content and '"parameters"' in content
|
||||||
# Check for calendar tool format: {"action": "...", ...}
|
# Check for calendar tool format: {"action": "...", ...}
|
||||||
has_calendar_format = '"action"' in content and ("calendar" in content.lower() or any(action in content for action in ["list_events", "create_event", "update_event", "delete_event"]))
|
has_calendar_format = '"action"' in content and ("calendar" in content.lower() or any(action in content for action in ["list_events", "create_event", "update_event", "delete_event"]))
|
||||||
|
|
||||||
if not tool_calls and content and (has_standard_format or has_calendar_format):
|
# Some backends will return *only* a JSON object as the entire message content.
|
||||||
|
# If it looks like a JSON object, attempt parsing even if our heuristics missed it.
|
||||||
|
looks_like_json_object = stripped.startswith("{") and stripped.endswith("}")
|
||||||
|
|
||||||
|
if not tool_calls and content and (has_standard_format or has_calendar_format or looks_like_json_object):
|
||||||
import re
|
import re
|
||||||
# Look for JSON tool call patterns: {"name": "exec", "parameters": {...}} or {"action": "list_events", ...}
|
# Look for JSON tool call patterns: {"name": "exec", "parameters": {...}} or {"action": "list_events", ...}
|
||||||
# Find complete JSON objects by matching braces
|
# Find complete JSON objects by matching braces
|
||||||
@ -131,28 +154,11 @@ class CustomProvider(LLMProvider):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle standard format: {"name": "...", "parameters": {...}}
|
# Handle standard format: {"name": "...", "parameters": {...}}
|
||||||
# Note: This list should match tools registered in AgentLoop._register_default_tools()
|
|
||||||
valid_tools = [
|
|
||||||
# File tools
|
|
||||||
"read_file", "write_file", "edit_file", "list_dir",
|
|
||||||
# Shell tool
|
|
||||||
"exec",
|
|
||||||
# Web tools
|
|
||||||
"web_search", "web_fetch",
|
|
||||||
# Communication tools
|
|
||||||
"message", "spawn",
|
|
||||||
# Calendar tool
|
|
||||||
"calendar",
|
|
||||||
# Cron tool
|
|
||||||
"cron",
|
|
||||||
# Email tool
|
|
||||||
"email",
|
|
||||||
]
|
|
||||||
if (isinstance(tool_obj, dict) and
|
if (isinstance(tool_obj, dict) and
|
||||||
"name" in tool_obj and
|
"name" in tool_obj and
|
||||||
"parameters" in tool_obj and
|
"parameters" in tool_obj and
|
||||||
isinstance(tool_obj["name"], str) and
|
isinstance(tool_obj["name"], str) and
|
||||||
tool_obj["name"] in valid_tools):
|
(tool_obj["name"] in valid_tools or tool_obj["name"].startswith("mcp_"))):
|
||||||
tool_calls.append(ToolCallRequest(
|
tool_calls.append(ToolCallRequest(
|
||||||
id=f"call_{len(tool_calls)}",
|
id=f"call_{len(tool_calls)}",
|
||||||
name=tool_obj["name"],
|
name=tool_obj["name"],
|
||||||
@ -166,6 +172,32 @@ class CustomProvider(LLMProvider):
|
|||||||
pass # If parsing fails, skip this match
|
pass # If parsing fails, skip this match
|
||||||
|
|
||||||
start_pos = json_start + 1 # Move past this match
|
start_pos = json_start + 1 # Move past this match
|
||||||
|
|
||||||
|
# If we still didn't match embedded objects, try parsing the whole message as a single tool-call JSON object.
|
||||||
|
if not tool_calls and looks_like_json_object:
|
||||||
|
try:
|
||||||
|
tool_obj = json_repair.loads(stripped)
|
||||||
|
if isinstance(tool_obj, dict) and "action" in tool_obj:
|
||||||
|
action = tool_obj.get("action")
|
||||||
|
if action and action in ["list_events", "create_event", "update_event", "delete_event", "delete_events", "check_availability"]:
|
||||||
|
tool_calls.append(ToolCallRequest(
|
||||||
|
id="call_0",
|
||||||
|
name="calendar",
|
||||||
|
arguments=tool_obj,
|
||||||
|
))
|
||||||
|
content = ""
|
||||||
|
if isinstance(tool_obj, dict) and "name" in tool_obj and "parameters" in tool_obj:
|
||||||
|
if isinstance(tool_obj["name"], str) and (
|
||||||
|
tool_obj["name"] in valid_tools or tool_obj["name"].startswith("mcp_")
|
||||||
|
):
|
||||||
|
tool_calls.append(ToolCallRequest(
|
||||||
|
id="call_0",
|
||||||
|
name=tool_obj["name"],
|
||||||
|
arguments=tool_obj["parameters"] if isinstance(tool_obj["parameters"], dict) else {"raw": str(tool_obj["parameters"])},
|
||||||
|
))
|
||||||
|
content = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
u = response.usage
|
u = response.usage
|
||||||
return LLMResponse(
|
return LLMResponse(
|
||||||
|
|||||||
113
scripts/setup-mcp-servers.sh
Executable file
113
scripts/setup-mcp-servers.sh
Executable file
@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Clone/build local MCP servers into ./mcp-servers (local-clone policy).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
MCP_DIR="${REPO_ROOT}/mcp-servers"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage:
|
||||||
|
./scripts/setup-mcp-servers.sh gitea
|
||||||
|
|
||||||
|
notes:
|
||||||
|
- clones into ./mcp-servers/<name>
|
||||||
|
- builds artifacts needed to run the MCP server locally
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||||
|
echo "error: missing '${cmd}' on PATH" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
need_go_min() {
|
||||||
|
local want_major="$1"
|
||||||
|
local want_minor="$2"
|
||||||
|
|
||||||
|
local v
|
||||||
|
v="$(go version 2>/dev/null || true)"
|
||||||
|
# Example: "go version go1.26.0 linux/amd64"
|
||||||
|
local ver
|
||||||
|
ver="$(echo "${v}" | awk '{print $3}' | sed 's/^go//')"
|
||||||
|
local major minor
|
||||||
|
major="$(echo "${ver}" | cut -d. -f1)"
|
||||||
|
minor="$(echo "${ver}" | cut -d. -f2)"
|
||||||
|
|
||||||
|
if [[ -z "${major}" || -z "${minor}" ]]; then
|
||||||
|
echo "error: could not parse Go version from: ${v}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare major/minor only (sufficient for our use).
|
||||||
|
if (( major < want_major )) || { (( major == want_major )) && (( minor < want_minor )); }; then
|
||||||
|
echo "error: Go ${want_major}.${want_minor}+ required; found ${ver}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_gitea() {
|
||||||
|
need_cmd git
|
||||||
|
|
||||||
|
if ! command -v go >/dev/null 2>&1; then
|
||||||
|
cat <<'EOF' >&2
|
||||||
|
error: Go toolchain not found (required to build gitea-mcp).
|
||||||
|
|
||||||
|
install one of:
|
||||||
|
- Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y golang
|
||||||
|
- Or install Go from https://go.dev/dl/
|
||||||
|
|
||||||
|
then rerun:
|
||||||
|
./scripts/setup-mcp-servers.sh gitea
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! need_go_min 1 26; then
|
||||||
|
cat <<'EOF' >&2
|
||||||
|
|
||||||
|
gitea-mcp currently requires a newer Go toolchain than Debian stable typically ships.
|
||||||
|
If you already installed a newer Go under /usr/local (example: /usr/local/go1.26/bin/go),
|
||||||
|
rerun with PATH overridden, e.g.:
|
||||||
|
|
||||||
|
PATH="/usr/local/go1.26/bin:$PATH" ./scripts/setup-mcp-servers.sh gitea
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${MCP_DIR}"
|
||||||
|
|
||||||
|
if [[ ! -d "${MCP_DIR}/gitea-mcp/.git" ]]; then
|
||||||
|
git clone https://gitea.com/gitea/gitea-mcp.git "${MCP_DIR}/gitea-mcp"
|
||||||
|
else
|
||||||
|
echo "info: gitea-mcp already cloned, skipping clone"
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd "${MCP_DIR}/gitea-mcp" && go build -o gitea-mcp .)
|
||||||
|
|
||||||
|
echo "done: built ${MCP_DIR}/gitea-mcp/gitea-mcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ "${#}" -ne 1 ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
gitea) setup_gitea ;;
|
||||||
|
-h|--help|help) usage ;;
|
||||||
|
*)
|
||||||
|
echo "error: unknown target '$1'" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user