Skip to content
Open
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
16 changes: 9 additions & 7 deletions integrations/catalog-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ import entry66 from "./catalog/memory.json" with { type: "json" };
import entry67 from "./catalog/mongodb.json" with { type: "json" };
import entry68 from "./catalog/neon.json" with { type: "json" };
import entry69 from "./catalog/obsidian.json" with { type: "json" };
import entry70 from "./catalog/paypal.json" with { type: "json" };
import entry71 from "./catalog/playwright.json" with { type: "json" };
import entry72 from "./catalog/redis.json" with { type: "json" };
import entry73 from "./catalog/resend.json" with { type: "json" };
import entry74 from "./catalog/sequential-thinking.json" with { type: "json" };
import entry75 from "./catalog/superhuman-mail.json" with { type: "json" };
import entry76 from "./catalog/time.json" with { type: "json" };
import entry70 from "./catalog/parallel-search.json" with { type: "json" };
import entry71 from "./catalog/paypal.json" with { type: "json" };
import entry72 from "./catalog/playwright.json" with { type: "json" };
import entry73 from "./catalog/redis.json" with { type: "json" };
import entry74 from "./catalog/resend.json" with { type: "json" };
import entry75 from "./catalog/sequential-thinking.json" with { type: "json" };
import entry76 from "./catalog/superhuman-mail.json" with { type: "json" };
import entry77 from "./catalog/time.json" with { type: "json" };

export const INTEGRATION_CATALOG_ENTRIES = [
entry0,
Expand Down Expand Up @@ -158,4 +159,5 @@ export const INTEGRATION_CATALOG_ENTRIES = [
entry74,
entry75,
entry76,
entry77,
];
26 changes: 26 additions & 0 deletions integrations/catalog/parallel-search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"id": "parallel-search",
"name": "Parallel Search",
"description": "Search the web and fetch page content with Parallel Search MCP. Free for light use, with no API key required.",
"docsUrl": "https://docs.parallel.ai/integrations/mcp/search-mcp",
"keywords": [
"search",
"web",
"research",
"fetch",
"extraction"
],
"connectionOptions": [
{
"id": "none",
"provider": "mcp",
"transport": {
"kind": "shttp",
"url": "https://search.parallel.ai/mcp"
},
"auth": {
"strategy": "none"
}
}
]
}
7 changes: 6 additions & 1 deletion tests/test_catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ def test_catalog_entries_have_required_fields():


def test_remote_no_auth_mcp_entries_are_intentionally_public():
public_remote_mcp_ids = {"cloudflare-docs", "deepwiki", "huggingface"}
public_remote_mcp_ids = {
"cloudflare-docs",
"deepwiki",
"huggingface",
"parallel-search",
}

actual = set()
for entry in load_catalog_entries("integrations/catalog"):
Expand Down
67 changes: 67 additions & 0 deletions tests/test_live_integration_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@

Evidence lines are printed as ``[OSS-5193] ...`` and contain only tool names,
counts, and provider error codes — never credentials or message content.

The anonymous Parallel Search smoke test needs no credentials and runs only
when explicitly enabled:

RUN_PARALLEL_MCP_LIVE=1 uv run pytest tests/test_live_integration_smoke.py -k parallel -s
"""

import json
Expand All @@ -33,6 +38,7 @@
import mcp.types
import pytest
from openhands.sdk.mcp import MCPError, MCPServer, MCPTimeoutError, create_mcp_tools
from openhands.sdk.mcp.definition import MCPToolObservation

from openhands_extensions import get_integration_catalog_entry_model

Expand Down Expand Up @@ -140,6 +146,67 @@ def _first_advertised(tool_names, candidates):
)


@pytest.mark.skipif(
os.environ.get("RUN_PARALLEL_MCP_LIVE") != "1",
reason="set RUN_PARALLEL_MCP_LIVE=1 to run the anonymous Parallel MCP smoke test",
)
def test_parallel_anonymous_search_fetch_and_invalid_arguments():
option = _installable_option("parallel-search")
assert option.auth.strategy == "none"
assert option.transport.kind == "shttp"
server = MCPServer.model_validate(
{"url": option.transport.url, "transport": "http"}
)

with create_mcp_tools({"parallel-search": server}, timeout=HTTP_TIMEOUT) as client:
assert {"web_search", "web_fetch"} <= {tool.name for tool in client.tools}
for name, arguments in (
(
"web_search",
{
"objective": "Find the official Python documentation for dictionaries.",
"search_queries": ["site:docs.python.org dictionary tutorial"],
},
),
(
"web_fetch",
{"urls": ["https://docs.python.org/3/tutorial/datastructures.html"]},
),
):
result = client.call_async_from_sync(
client.call_tool_mcp,
name=name,
arguments=arguments,
timeout=HTTP_TIMEOUT,
)
assert not result.isError
text = "\n".join(
block.text
for block in result.content
if isinstance(block, mcp.types.TextContent)
)
payload = json.loads(text)
assert payload == result.structuredContent
assert payload["results"]
assert all(
item["url"].startswith(("http://", "https://"))
for item in payload["results"]
)
assert any(item.get("excerpts") for item in payload["results"])
observation = MCPToolObservation.from_call_tool_result(name, result)
assert not observation.is_error
assert text in [block.text for block in observation.content]

invalid = client.call_async_from_sync(
client.call_tool_mcp,
name="web_search",
arguments={"objective": "Find Python documentation"},
timeout=HTTP_TIMEOUT,
)
assert invalid.isError
assert MCPToolObservation.from_call_tool_result("web_search", invalid).is_error


@requires_github
def test_github_identity_and_pr_read():
option = _installable_option("github")
Expand Down
Loading