Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b9b777
feat: add support for searching using gemini 2.5 flash
Justxd22 Feb 12, 2026
810e2b5
feat: add web search mcp, add instructions
Justxd22 Feb 12, 2026
f657f95
fix: address PR #286 review feedback
Justxd22 Mar 2, 2026
8976aa3
fix: address web-search model routing and MCP issues
ayuuuvauuu Mar 13, 2026
2de7c66
Merge branch 'main' into feat/web-search to get the latest changes
ayuuuvauuu Mar 13, 2026
76e3d6a
build: update package-lock.json dependencies
ayuuuvauuu Mar 13, 2026
f366335
fix: rewrote web search tool to use official python sdk
ayuuuvauuu Mar 13, 2026
9c885f5
perf(mcp): optimize web search payload for latency and token usage
ayuuuvauuu Mar 13, 2026
a1219b4
fix(mcp): add thinking disabled flag to search payload
ayuuuvauuu Mar 13, 2026
7088162
feat(proxy): respect thinking disabled flag in request converter
ayuuuvauuu Mar 13, 2026
14e3ae7
fix: rewrote web search tool to use official python sdk
ayuuuvauuu Mar 13, 2026
bfdab2d
fix(mcp): log stack traces to stderr instead of leaking to client
ayuuuvauuu Mar 13, 2026
ca3d9a0
fix(mcp): avoid blocking event loop during DuckDuckGo search
ayuuuvauuu Mar 13, 2026
cd81b96
fix(mcp): correct search tool description to say DuckDuckGo
ayuuuvauuu Mar 13, 2026
53f9518
fix(mcp): add query length validation to search tool
ayuuuvauuu Mar 13, 2026
79610ea
build(mcp): pin Python dependencies to compatible-release ranges
ayuuuvauuu Mar 13, 2026
6da2448
feat(mcp): add web search MCP server using Google Search grounding
ayuuuvauuu Mar 24, 2026
d51a542
test(mcp): add web search integration tests
ayuuuvauuu Mar 24, 2026
7610b1c
Merge branch 'feat/web-search' of github.com:ayuuuvauuu/antigravity-c…
ayuuuvauuu Mar 24, 2026
d47e1e7
feat(proxy): add Google Search grounding support and rewrite MCP server
ayuuuvauuu Mar 24, 2026
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
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,44 @@ npm run test:caching # Prompt caching
npm run test:crossmodel # Cross-model thinking signatures
npm run test:oauth # OAuth no-browser mode
npm run test:cache-control # Cache control field stripping
npm run test:websearch # Web search MCP (Google Search grounding)

# Run strategy unit tests (no server required)
node tests/test-strategies.cjs
```

## Web Search MCP Server

An MCP server that provides Google Search grounding via Gemini through the proxy.

**Setup:** Add to your Claude Code project config (`~/.claude.json` under `projects.<path>.mcpServers`):

```json
{
"mcpServers": {
"antigravity-search": {
"type": "stdio",
"command": "python3",
"args": ["./scripts/web_search_mcp.py"]
}
}
}
```

**Dependencies:** Install Python dependencies before first use:

```bash
pip install -r scripts/requirements.txt
```

**How it works:** Sends queries to `gemini-3-flash` through the proxy with a `google_search` tool that activates Google Search grounding, plus a minimal thinking budget (`budget_tokens: 1`) for fast responses. Returns live search results, not training data.

**Google Search Grounding (Proxy-level):**
- Any Anthropic-format request can enable grounding by including a tool named `google_search` or `googleSearchRetrieval`
- The proxy converts these to native Gemini `{ google_search: {} }` entries, separate from `functionDeclarations`
- Grounding cannot be mixed with function declarations in the same request (Cloud Code API limitation)
- Grounding is only supported on Gemini models

## Architecture

**Request Flow:**
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"test:emptyretry": "node tests/test-empty-response-retry.cjs",
"test:sanitizer": "node tests/test-schema-sanitizer.cjs",
"test:strategies": "node tests/test-strategies.cjs",
"test:cache-control": "node tests/test-cache-control.cjs"
"test:cache-control": "node tests/test-cache-control.cjs",
"test:websearch": "node tests/test-web-search.cjs"
},
"keywords": [
"claude",
Expand Down
2 changes: 2 additions & 0 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mcp~=1.0.0
requests~=2.28.0
116 changes: 116 additions & 0 deletions scripts/web_search_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import sys
import json
import asyncio
import traceback
import os
import requests
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types


def get_proxy_config():
"""Read proxy URL and API key from Claude CLI settings."""
config_path = os.path.join(os.path.expanduser("~"), ".claude", "settings.json")
try:
with open(config_path) as f:
config = json.load(f)
base_url = config.get("apiBaseUrl", "http://localhost:8080")
api_key = config.get("apiKey", "test")
return f"{base_url}/v1/messages", api_key
except Exception:
return "http://localhost:8080/v1/messages", "test"


PROXY_URL, API_KEY = get_proxy_config()

app = Server("antigravity-search")


@app.list_tools()
async def list_tools() -> list[types.Tool]:
"""List available tools."""
return [
types.Tool(
name="search",
description="Performs a web search via Gemini's Google Search grounding through the Antigravity Proxy.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}
)
]


def _call_proxy(query: str) -> str:
"""Send a search query through the Antigravity Proxy using Google Search grounding."""
headers = {"x-api-key": API_KEY, "Content-Type": "application/json"}
payload = {
"model": "gemini-3-flash",
"system": "You are a concise search assistant. Return ONLY factual results in 2-3 sentences with source URLs. No code, no filler.",
"messages": [{"role": "user", "content": query}],
"max_tokens": 512,
"thinking": {"budget_tokens": 1},
"tools": [{"name": "google_search", "input_schema": {"type": "object"}}]
}

response = requests.post(PROXY_URL, headers=headers, json=payload, timeout=60)

if response.status_code != 200:
return f"Error: Proxy returned status {response.status_code} - {response.text}"

data = response.json()
content_blocks = data.get("content", [])
text_parts = [block.get("text", "") for block in content_blocks if block.get("type") == "text"]
return "".join(text_parts) if text_parts else "No results found."


@app.call_tool()
async def call_tool(name: str, arguments: dict):
"""Handle tool calls."""
if name != "search":
raise ValueError(f"Unknown tool: {name}")

query = arguments.get("query")
if not query:
raise ValueError("Missing required parameter 'query'")

if len(query) > 500:
raise ValueError("Query too long (max 500 characters)")

try:
result = await asyncio.to_thread(_call_proxy, query)
return [types.TextContent(type="text", text=result)]
except Exception as e:
sys.stderr.write(f"Search error: {traceback.format_exc()}\n")
sys.stderr.flush()
return [types.TextContent(type="text", text=f"Search failed: {str(e)}")]


async def main():
sys.stderr.write("Starting Antigravity Search MCP Server...\n")
sys.stderr.flush()
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="antigravity-search",
server_version="1.1.0",
capabilities=app.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
)
),
)


if __name__ == "__main__":
asyncio.run(main())
95 changes: 62 additions & 33 deletions src/format/request-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,43 +202,72 @@ export function convertAnthropicToGoogle(anthropicRequest) {

// Convert tools to Google format
if (tools && tools.length > 0) {
const functionDeclarations = tools.map((tool, idx) => {
// Extract name from various possible locations
const name = tool.name || tool.function?.name || tool.custom?.name || `tool-${idx}`;

// Extract description from various possible locations
const description = tool.description || tool.function?.description || tool.custom?.description || '';

// Extract schema from various possible locations
const schema = tool.input_schema
|| tool.function?.input_schema
|| tool.function?.parameters
|| tool.custom?.input_schema
|| tool.parameters
|| { type: 'object' };

// Sanitize schema for general compatibility
let parameters = sanitizeSchema(schema);

// Apply Google-format cleaning for ALL models since they all go through
// Cloud Code API which validates schemas using Google's protobuf format.
// This fixes issue #82: /compact command fails with schema transformation error
// "Proto field is not repeating, cannot start list" for Claude models.
parameters = cleanSchema(parameters);

return {
name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
description: description,
parameters
};
});
// Separate Google Search grounding tools from regular function declarations.
// Clients signal grounding by including a tool named "google_search" or
// "googleSearchRetrieval" in the Anthropic tools array. These are converted
// to native Gemini grounding entries instead of functionDeclarations.
const GROUNDING_TOOL_NAMES = new Set(['google_search', 'googleSearchRetrieval']);
const regularTools = [];
const groundingEntries = [];

for (const tool of tools) {
const name = tool.name || tool.function?.name || tool.custom?.name || '';
if (GROUNDING_TOOL_NAMES.has(name)) {
groundingEntries.push({ google_search: {} });
logger.debug('[RequestConverter] Google Search grounding enabled');
} else {
regularTools.push(tool);
}
}

googleRequest.tools = [{ functionDeclarations }];
logger.debug(`[RequestConverter] Tools: ${JSON.stringify(googleRequest.tools).substring(0, 300)}`);
const googleTools = [];

if (regularTools.length > 0) {
const functionDeclarations = regularTools.map((tool, idx) => {
// Extract name from various possible locations
const name = tool.name || tool.function?.name || tool.custom?.name || `tool-${idx}`;

// Extract description from various possible locations
const description = tool.description || tool.function?.description || tool.custom?.description || '';

// Extract schema from various possible locations
const schema = tool.input_schema
|| tool.function?.input_schema
|| tool.function?.parameters
|| tool.custom?.input_schema
|| tool.parameters
|| { type: 'object' };

// Sanitize schema for general compatibility
let parameters = sanitizeSchema(schema);

// Apply Google-format cleaning for ALL models since they all go through
// Cloud Code API which validates schemas using Google's protobuf format.
// This fixes issue #82: /compact command fails with schema transformation error
// "Proto field is not repeating, cannot start list" for Claude models.
parameters = cleanSchema(parameters);

return {
name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
description: description,
parameters
};
});

googleTools.push({ functionDeclarations });
}

// Append grounding tools as separate entries in the tools array
googleTools.push(...groundingEntries);

if (googleTools.length > 0) {
googleRequest.tools = googleTools;
logger.debug(`[RequestConverter] Tools: ${JSON.stringify(googleRequest.tools).substring(0, 300)}`);
}

// For Claude models, set functionCallingConfig.mode = "VALIDATED"
// This ensures strict parameter validation (matches opencode-antigravity-auth)
if (isClaudeModel) {
if (isClaudeModel && regularTools.length > 0) {
googleRequest.toolConfig = {
functionCallingConfig: {
mode: 'VALIDATED'
Expand Down
Loading