Skip to content
Closed
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
2 changes: 1 addition & 1 deletion dashboard/src/api/modules/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ConnectorCatalogEntry {
icon: string;
color: string;
phase: "available" | "coming_soon";
mcp_mode: "remote" | "gateway" | "internal";
mcp_mode: "remote" | "gateway" | "internal" | "stdio";
category: ConnectorCategory;
quick_auth_url?: string | null;
login_url?: string | null;
Expand Down
14 changes: 14 additions & 0 deletions src/octop/infra/connectors/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ def _build_stdio_spec(entry: ConnectorCatalogEntry, creds: dict[str, Any]) -> di
creds.get("token") or creds.get("api_key") or creds.get("personal_token") or ""
).strip()
base_url = str(creds.get("base_url") or "https://www.yuque.com/api/v2").strip()
# The Yuque Open API lives under /api/v2; a bare site root would send
# requests to the marketing homepage, so append the path when missing.
if not base_url.endswith("/api/v2"):
base_url = base_url.rstrip("/") + "/api/v2"
return {
"transport": "stdio",
"command": "npx",
Expand Down Expand Up @@ -529,6 +533,16 @@ def _redact_mcp_configs_for_log(configs: dict[str, Any]) -> dict[str, Any]:
if key in redacted:
redacted[key] = "***"
entry["headers"] = redacted
env = entry.get("env")
if isinstance(env, dict):
redacted_env = dict(env)
for env_key in redacted_env:
if any(
marker in env_key.upper()
for marker in ("TOKEN", "SECRET", "KEY", "PASSWORD", "CREDENTIAL")
):
redacted_env[env_key] = "***"
entry["env"] = redacted_env
for key, value in list(entry.items()):
if callable(value):
entry[key] = f"<callable {getattr(value, '__name__', type(value).__name__)}>"
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/test_connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,53 @@ def test_dify_builds_streamable_http_spec():
}


def test_yuque_mcp_builds_stdio_spec():
entry = get_catalog_entry("yuque-mcp")
assert entry is not None
assert entry.mcp_mode == "stdio"
creds = validate_create_credentials("yuque-mcp", {"token": "yuque-token"})
assert creds == {"token": "yuque-token"}
spec = build_http_mcp_spec(
entry=entry,
instance_id="x",
creds=creds,
config=OctopConfig(),
)
assert spec == {
"transport": "stdio",
"command": "npx",
"args": ["-y", "yuque-mcp"],
"env": {
"YUQUE_PERSONAL_TOKEN": "yuque-token",
"YUQUE_BASE_URL": "https://www.yuque.com/api/v2",
},
}


def test_yuque_mcp_stdio_spec_appends_api_v2_to_site_root():
entry = get_catalog_entry("yuque-mcp")
assert entry is not None
spec = build_http_mcp_spec(
entry=entry,
instance_id="x",
creds={"token": "t", "base_url": "https://www.yuque.com"},
config=OctopConfig(),
)
assert spec["env"]["YUQUE_BASE_URL"] == "https://www.yuque.com/api/v2"


def test_yuque_mcp_stdio_spec_keeps_explicit_api_v2():
entry = get_catalog_entry("yuque-mcp")
assert entry is not None
spec = build_http_mcp_spec(
entry=entry,
instance_id="x",
creds={"token": "t", "base_url": "https://www.yuque.com/api/v2"},
config=OctopConfig(),
)
assert spec["env"]["YUQUE_BASE_URL"] == "https://www.yuque.com/api/v2"


def test_dify_rejects_non_server_url():
with pytest.raises(ValueError, match="Dify MCP Server URL"):
validate_create_credentials("dify", {"mcp_url": "https://dify.example.com/v1/chat"})
Expand All @@ -306,6 +353,27 @@ def test_dify_server_code_is_redacted_from_logs():
assert redacted["dify__x"]["url"] == "https://dify.example.com/mcp/server/***/mcp"


def test_stdio_spec_env_tokens_are_redacted_from_logs():
from octop.infra.connectors.builder import _redact_mcp_configs_for_log

redacted = _redact_mcp_configs_for_log(
{
"yuque-mcp__x": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "yuque-mcp"],
"env": {
"YUQUE_PERSONAL_TOKEN": "top-secret-token",
"YUQUE_BASE_URL": "https://www.yuque.com/api/v2",
},
}
}
)
env = redacted["yuque-mcp__x"]["env"]
assert env["YUQUE_PERSONAL_TOKEN"] == "***"
assert env["YUQUE_BASE_URL"] == "https://www.yuque.com/api/v2"


def test_didi_builds_streamable_http_spec_from_user_key():
entry = get_catalog_entry("didi")
assert entry is not None
Expand Down
Loading