mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Compare commits
2 Commits
hotfix/sec
...
copilot/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e927671a | ||
|
|
1750c833ee |
@@ -1,9 +1,12 @@
|
||||
"""
|
||||
End-to-end tests against a real public MCP server.
|
||||
End-to-end tests against real public and authenticated MCP servers.
|
||||
|
||||
These tests hit the OpenAI docs MCP server (https://developers.openai.com/mcp)
|
||||
which is publicly accessible without authentication and returns SSE responses.
|
||||
These tests hit live MCP servers and require network access:
|
||||
- OpenAI docs (https://developers.openai.com/mcp) — no auth required
|
||||
- Sentry (https://mcp.sentry.dev/mcp) — requires SENTRY_MCP_TOKEN env var
|
||||
- Linear (https://mcp.linear.app/mcp) — requires LINEAR_MCP_TOKEN env var
|
||||
|
||||
All tests are skipped unless the respective environment variables are set.
|
||||
Mark: These are tagged with ``@pytest.mark.e2e`` so they can be run/skipped
|
||||
independently of the rest of the test suite (they require network access).
|
||||
"""
|
||||
@@ -18,6 +21,10 @@ from backend.blocks.mcp.client import MCPClient
|
||||
# Public MCP server that requires no authentication
|
||||
OPENAI_DOCS_MCP_URL = "https://developers.openai.com/mcp"
|
||||
|
||||
# Authenticated MCP servers
|
||||
SENTRY_MCP_URL = "https://mcp.sentry.dev/mcp"
|
||||
LINEAR_MCP_URL = "https://mcp.linear.app/mcp"
|
||||
|
||||
# Skip all tests in this module unless RUN_E2E env var is set
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("RUN_E2E"), reason="set RUN_E2E=1 to run e2e tests"
|
||||
@@ -107,3 +114,120 @@ class TestRealMCPServer:
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) > 0
|
||||
assert all(hasattr(t, "name") for t in tools)
|
||||
|
||||
|
||||
def _assert_has_relevant_tool(tool_names: set[str], keywords: list[str], server: str) -> None:
|
||||
"""Assert that at least one tool name contains one of the expected keywords."""
|
||||
assert any(
|
||||
kw in name.lower() for name in tool_names for kw in keywords
|
||||
), f"Expected a tool containing {keywords} on {server}, got: {sorted(tool_names)}"
|
||||
|
||||
|
||||
class TestSentryMCPServer:
|
||||
"""Tests against the live Sentry MCP server (https://mcp.sentry.dev/mcp).
|
||||
|
||||
Requires SENTRY_MCP_TOKEN to be set to a valid Sentry OAuth access token.
|
||||
Skipped automatically when the token is not present.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def sentry_token(self) -> str:
|
||||
token = os.environ.get("SENTRY_MCP_TOKEN")
|
||||
if not token:
|
||||
pytest.skip("SENTRY_MCP_TOKEN not set")
|
||||
return token
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_initialize(self, sentry_token):
|
||||
"""Verify we can complete the MCP handshake with Sentry."""
|
||||
client = MCPClient(SENTRY_MCP_URL, auth_token=sentry_token)
|
||||
result = await client.initialize()
|
||||
|
||||
assert "protocolVersion" in result
|
||||
assert "serverInfo" in result
|
||||
assert "tools" in result.get("capabilities", {})
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_list_tools(self, sentry_token):
|
||||
"""Verify we can discover tools from the Sentry MCP server."""
|
||||
client = MCPClient(SENTRY_MCP_URL, auth_token=sentry_token)
|
||||
await client.initialize()
|
||||
tools = await client.list_tools()
|
||||
|
||||
assert len(tools) >= 1
|
||||
tool_names = {t.name for t in tools}
|
||||
_assert_has_relevant_tool(
|
||||
tool_names, ["issue", "event", "error"], "mcp.sentry.dev"
|
||||
)
|
||||
|
||||
for tool in tools:
|
||||
assert tool.name
|
||||
assert isinstance(tool.input_schema, dict)
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_sse_response_handling(self, sentry_token):
|
||||
"""Verify the client handles SSE responses from the Sentry server."""
|
||||
client = MCPClient(SENTRY_MCP_URL, auth_token=sentry_token)
|
||||
result = await client.initialize()
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert "protocolVersion" in result
|
||||
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) > 0
|
||||
assert all(hasattr(t, "name") for t in tools)
|
||||
|
||||
|
||||
class TestLinearMCPServer:
|
||||
"""Tests against the live Linear MCP server (https://mcp.linear.app/mcp).
|
||||
|
||||
Requires LINEAR_MCP_TOKEN to be set to a valid Linear OAuth access token.
|
||||
Skipped automatically when the token is not present.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def linear_token(self) -> str:
|
||||
token = os.environ.get("LINEAR_MCP_TOKEN")
|
||||
if not token:
|
||||
pytest.skip("LINEAR_MCP_TOKEN not set")
|
||||
return token
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_initialize(self, linear_token):
|
||||
"""Verify we can complete the MCP handshake with Linear."""
|
||||
client = MCPClient(LINEAR_MCP_URL, auth_token=linear_token)
|
||||
result = await client.initialize()
|
||||
|
||||
assert "protocolVersion" in result
|
||||
assert "serverInfo" in result
|
||||
assert "tools" in result.get("capabilities", {})
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_list_tools(self, linear_token):
|
||||
"""Verify we can discover tools from the Linear MCP server."""
|
||||
client = MCPClient(LINEAR_MCP_URL, auth_token=linear_token)
|
||||
await client.initialize()
|
||||
tools = await client.list_tools()
|
||||
|
||||
assert len(tools) >= 1
|
||||
tool_names = {t.name for t in tools}
|
||||
_assert_has_relevant_tool(
|
||||
tool_names, ["issue", "project", "team"], "mcp.linear.app"
|
||||
)
|
||||
|
||||
for tool in tools:
|
||||
assert tool.name
|
||||
assert isinstance(tool.input_schema, dict)
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_sse_response_handling(self, linear_token):
|
||||
"""Verify the client handles SSE responses from the Linear server."""
|
||||
client = MCPClient(LINEAR_MCP_URL, auth_token=linear_token)
|
||||
result = await client.initialize()
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert "protocolVersion" in result
|
||||
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) > 0
|
||||
assert all(hasattr(t, "name") for t in tools)
|
||||
|
||||
@@ -15,6 +15,7 @@ Use these URLs directly without asking the user:
|
||||
|---|---|
|
||||
| Notion | `https://mcp.notion.com/mcp` |
|
||||
| Linear | `https://mcp.linear.app/mcp` |
|
||||
| Sentry | `https://mcp.sentry.dev/mcp` |
|
||||
| Stripe | `https://mcp.stripe.com` |
|
||||
| Intercom | `https://mcp.intercom.com/mcp` |
|
||||
| Cloudflare | `https://mcp.cloudflare.com/mcp` |
|
||||
|
||||
@@ -38,24 +38,12 @@ def test_sdk_exports_message_types():
|
||||
|
||||
|
||||
def test_sdk_exports_content_block_types():
|
||||
from claude_agent_sdk import TextBlock, ThinkingBlock, ToolResultBlock, ToolUseBlock
|
||||
from claude_agent_sdk import TextBlock, ToolResultBlock, ToolUseBlock
|
||||
|
||||
for cls in (TextBlock, ThinkingBlock, ToolResultBlock, ToolUseBlock):
|
||||
for cls in (TextBlock, ToolResultBlock, ToolUseBlock):
|
||||
assert inspect.isclass(cls), f"{cls.__name__} is not a class"
|
||||
|
||||
|
||||
def test_thinking_block_has_required_fields():
|
||||
"""ThinkingBlock must have thinking + signature for API integrity on resume."""
|
||||
import dataclasses
|
||||
|
||||
from claude_agent_sdk import ThinkingBlock
|
||||
|
||||
assert dataclasses.is_dataclass(ThinkingBlock)
|
||||
field_names = {f.name for f in dataclasses.fields(ThinkingBlock)}
|
||||
assert "thinking" in field_names, "ThinkingBlock missing 'thinking' field"
|
||||
assert "signature" in field_names, "ThinkingBlock missing 'signature' field"
|
||||
|
||||
|
||||
def test_sdk_exports_mcp_helpers():
|
||||
from claude_agent_sdk import create_sdk_mcp_server, tool
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import shutil
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import dataclasses
|
||||
from collections.abc import AsyncGenerator, AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
||||
@@ -596,9 +595,7 @@ def _format_sdk_content_blocks(blocks: list) -> list[dict[str, Any]]:
|
||||
"""Convert SDK content blocks to transcript format.
|
||||
|
||||
Handles TextBlock, ToolUseBlock, ToolResultBlock, and ThinkingBlock.
|
||||
Unknown block types are preserved via dataclass/dict passthrough so that
|
||||
the Anthropic API's integrity checks pass on session resume (thinking and
|
||||
redacted_thinking blocks must be byte-identical to the original response).
|
||||
Unknown block types are logged and skipped.
|
||||
"""
|
||||
result: list[dict[str, Any]] = []
|
||||
for block in blocks or []:
|
||||
@@ -623,55 +620,18 @@ def _format_sdk_content_blocks(blocks: list) -> list[dict[str, Any]]:
|
||||
tool_result_entry["is_error"] = True
|
||||
result.append(tool_result_entry)
|
||||
elif isinstance(block, ThinkingBlock):
|
||||
# Preserve ALL fields exactly as received — the Anthropic API
|
||||
# validates that thinking blocks are unmodified on session resume.
|
||||
# Using dataclasses.asdict() ensures any future fields added to
|
||||
# ThinkingBlock are automatically preserved.
|
||||
entry = dataclasses.asdict(block)
|
||||
entry["type"] = "thinking"
|
||||
result.append(entry)
|
||||
else:
|
||||
# Preserve unknown block types (e.g. redacted_thinking) via
|
||||
# generic passthrough rather than dropping them — dropped blocks
|
||||
# cause index shifts that break the API's thinking block
|
||||
# integrity check on resume.
|
||||
if dataclasses.is_dataclass(block) and not isinstance(block, type):
|
||||
entry = dataclasses.asdict(block)
|
||||
if "type" not in entry:
|
||||
# Derive type from class name: RedactedThinkingBlock
|
||||
# -> redacted_thinking, etc.
|
||||
cls_name = type(block).__name__
|
||||
entry["type"] = re.sub(
|
||||
r"(?<=[a-z])(?=[A-Z])", "_", cls_name.removesuffix("Block")
|
||||
).lower()
|
||||
result.append(entry)
|
||||
logger.info(
|
||||
"[SDK] Preserved unknown block type via passthrough: %s",
|
||||
type(block).__name__,
|
||||
)
|
||||
elif hasattr(block, "__dict__"):
|
||||
entry = {
|
||||
k: v
|
||||
for k, v in block.__dict__.items()
|
||||
if not k.startswith("_")
|
||||
result.append(
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": block.thinking,
|
||||
"signature": block.signature,
|
||||
}
|
||||
if "type" not in entry:
|
||||
cls_name = type(block).__name__
|
||||
entry["type"] = re.sub(
|
||||
r"(?<=[a-z])(?=[A-Z])", "_", cls_name.removesuffix("Block")
|
||||
).lower()
|
||||
result.append(entry)
|
||||
logger.info(
|
||||
"[SDK] Preserved unknown block type via __dict__: %s",
|
||||
type(block).__name__,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[SDK] Dropping unserializable content block type: %s. "
|
||||
"This may break session resume if this block type is "
|
||||
"subject to API integrity checks.",
|
||||
type(block).__name__,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[SDK] Unknown content block type: {type(block).__name__}. "
|
||||
f"This may indicate a new SDK version with additional block types."
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Base stage for both dev and prod
|
||||
FROM node:21-alpine AS base
|
||||
FROM node:22.22-alpine3.23 AS base
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
COPY autogpt_platform/frontend/package.json autogpt_platform/frontend/pnpm-lock.yaml ./
|
||||
@@ -33,7 +33,7 @@ ENV NEXT_PUBLIC_SOURCEMAPS="false"
|
||||
RUN if [ "$NEXT_PUBLIC_PW_TEST" = "true" ]; then NEXT_PUBLIC_PW_TEST=true NODE_OPTIONS="--max-old-space-size=8192" pnpm build; else NODE_OPTIONS="--max-old-space-size=8192" pnpm build; fi
|
||||
|
||||
# Prod stage - based on NextJS reference Dockerfile https://github.com/vercel/next.js/blob/64271354533ed16da51be5dce85f0dbd15f17517/examples/with-docker/Dockerfile
|
||||
FROM node:21-alpine AS prod
|
||||
FROM node:22.22-alpine3.23 AS prod
|
||||
ENV NODE_ENV=production
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
WORKDIR /app
|
||||
|
||||
Reference in New Issue
Block a user