@jhzhu89/mcp-server-web-search 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/package.json +42 -8
- package/src/azure-openai.ts +49 -0
- package/src/index.ts +25 -0
- package/src/server.ts +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @jhzhu89/mcp-server-web-search
|
|
2
|
+
|
|
3
|
+
MCP Server for web search via Azure OpenAI Responses API with `web_search_preview` tool.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- [Bun](https://bun.sh) runtime installed
|
|
10
|
+
|
|
11
|
+
### Claude Code (Recommended)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Install globally (available in all projects)
|
|
15
|
+
claude mcp add web-search bunx @jhzhu89/mcp-server-web-search \
|
|
16
|
+
-s user \
|
|
17
|
+
-e AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com \
|
|
18
|
+
-e AZURE_OPENAI_DEPLOYMENT=gpt-5-mini
|
|
19
|
+
|
|
20
|
+
# Or install for current project only (default)
|
|
21
|
+
claude mcp add web-search bunx @jhzhu89/mcp-server-web-search \
|
|
22
|
+
-e AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com \
|
|
23
|
+
-e AZURE_OPENAI_DEPLOYMENT=gpt-5-mini
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Manual Configuration
|
|
27
|
+
|
|
28
|
+
Add to your `.mcp.json`:
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"web-search": {
|
|
33
|
+
"type": "stdio",
|
|
34
|
+
"command": "bunx",
|
|
35
|
+
"args": ["@jhzhu89/mcp-server-web-search"],
|
|
36
|
+
"env": {
|
|
37
|
+
"AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com",
|
|
38
|
+
"AZURE_OPENAI_DEPLOYMENT": "gpt-5-mini"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Environment Variables
|
|
46
|
+
|
|
47
|
+
| Variable | Required | Default | Description |
|
|
48
|
+
|----------|----------|---------|-------------|
|
|
49
|
+
| `AZURE_OPENAI_ENDPOINT` | ✓ | - | Azure OpenAI endpoint URL |
|
|
50
|
+
| `AZURE_OPENAI_DEPLOYMENT` | | `gpt-5-mini` | Deployment name |
|
|
51
|
+
| `AZURE_OPENAI_API_VERSION` | | `2025-03-01-preview` | API version |
|
|
52
|
+
|
|
53
|
+
## Authentication
|
|
54
|
+
|
|
55
|
+
This server uses [DefaultAzureCredential](https://learn.microsoft.com/en-us/javascript/api/@azure/identity/defaultazurecredential) for Azure authentication. Supported methods:
|
|
56
|
+
|
|
57
|
+
1. **Azure CLI** (local development): Run `az login` before starting
|
|
58
|
+
2. **Environment variables**: Set `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`
|
|
59
|
+
3. **Managed Identity**: Works automatically in Azure environments
|
|
60
|
+
|
|
61
|
+
## Tool: web_search
|
|
62
|
+
|
|
63
|
+
Search the web for real-time information with source citations.
|
|
64
|
+
|
|
65
|
+
### Input
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Required | Description |
|
|
68
|
+
|-----------|------|----------|-------------|
|
|
69
|
+
| `query` | string | ✓ | Search query |
|
|
70
|
+
| `search_context_size` | `"low"` \| `"medium"` \| `"high"` | | Amount of context (default: `"medium"`) |
|
|
71
|
+
| `user_location` | object | | `{type, city, country, region, timezone}` for localized results |
|
|
72
|
+
|
|
73
|
+
### Output (structuredContent)
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"results": [
|
|
78
|
+
{
|
|
79
|
+
"query": "original search query",
|
|
80
|
+
"text": "Answer text with inline [citations](url)...",
|
|
81
|
+
"citations": [
|
|
82
|
+
{
|
|
83
|
+
"title": "Source Title",
|
|
84
|
+
"url": "https://example.com",
|
|
85
|
+
"start_index": 0,
|
|
86
|
+
"end_index": 100
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Clone and install
|
|
98
|
+
bun install
|
|
99
|
+
|
|
100
|
+
# Run locally
|
|
101
|
+
export AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
|
|
102
|
+
az login
|
|
103
|
+
bun run start
|
|
104
|
+
|
|
105
|
+
# Run with HTTP transport
|
|
106
|
+
bun run start:http
|
|
107
|
+
|
|
108
|
+
# Test
|
|
109
|
+
bun test
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@jhzhu89/mcp-server-web-search",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@jhzhu89/mcp-server-web-search",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP Server providing web search via Azure OpenAI Responses API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Jiahao Zhu <jiahzhu@outlook.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/jhzhu89/mcp-server-web-search"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"mcp-server-web-search": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "bun run src/index.ts",
|
|
20
|
+
"start:http": "bun run src/index.ts -- --http",
|
|
21
|
+
"dev": "bun --watch run src/index.ts -- --http",
|
|
22
|
+
"build": "bun build src/index.ts --outdir dist --target bun",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"format": "prettier --write src test",
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"release:patch": "npm version patch && git push && git push --tags",
|
|
27
|
+
"release:minor": "npm version minor && git push && git push --tags",
|
|
28
|
+
"release:major": "npm version major && git push && git push --tags"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@azure/identity": "^4.13.0",
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
33
|
+
"openai": "^6.16.0",
|
|
34
|
+
"zod": "^3.25.61"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "^9.28.0",
|
|
38
|
+
"@types/bun": "latest",
|
|
39
|
+
"dotenv": "^17.2.3",
|
|
40
|
+
"typescript-eslint": "^8.33.1"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AzureOpenAI } from "openai";
|
|
2
|
+
import {
|
|
3
|
+
DefaultAzureCredential,
|
|
4
|
+
getBearerTokenProvider,
|
|
5
|
+
} from "@azure/identity";
|
|
6
|
+
import type {
|
|
7
|
+
Response,
|
|
8
|
+
WebSearchPreviewTool,
|
|
9
|
+
} from "openai/resources/responses/responses";
|
|
10
|
+
|
|
11
|
+
const credential = new DefaultAzureCredential();
|
|
12
|
+
const client = new AzureOpenAI({
|
|
13
|
+
azureADTokenProvider: getBearerTokenProvider(
|
|
14
|
+
credential,
|
|
15
|
+
"https://cognitiveservices.azure.com/.default",
|
|
16
|
+
),
|
|
17
|
+
apiVersion: process.env.AZURE_OPENAI_API_VERSION ?? "2025-03-01-preview",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const deployment = process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-5-mini";
|
|
21
|
+
|
|
22
|
+
export async function webSearch(
|
|
23
|
+
query: string,
|
|
24
|
+
searchContextSize: "low" | "medium" | "high" = "medium",
|
|
25
|
+
userLocation?: WebSearchPreviewTool.UserLocation,
|
|
26
|
+
): Promise<Response> {
|
|
27
|
+
return client.responses.create({
|
|
28
|
+
model: deployment,
|
|
29
|
+
input: query,
|
|
30
|
+
instructions: `You are a web search tool. Your ONLY job is to search the web and return the search results directly.
|
|
31
|
+
|
|
32
|
+
CRITICAL RULES:
|
|
33
|
+
- NEVER ask clarifying questions or seek confirmation
|
|
34
|
+
- NEVER offer options like "Which would you like?" or "Do you want me to..."
|
|
35
|
+
- NEVER use conversational phrases or confirmatory language
|
|
36
|
+
- ALWAYS perform the search immediately and return the results
|
|
37
|
+
- Present information directly and factually from search results
|
|
38
|
+
- Include relevant citations and sources
|
|
39
|
+
- Be comprehensive but concise`,
|
|
40
|
+
tools: [
|
|
41
|
+
{
|
|
42
|
+
type: "web_search_preview",
|
|
43
|
+
search_context_size: searchContextSize,
|
|
44
|
+
user_location: userLocation,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
tool_choice: { type: "web_search_preview" },
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { server } from "./server.js";
|
|
2
|
+
|
|
3
|
+
if (process.argv.includes("--http")) {
|
|
4
|
+
const { WebStandardStreamableHTTPServerTransport } =
|
|
5
|
+
await import("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
6
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
7
|
+
const port = Number(process.env.MCP_SERVER_PORT) || 3001;
|
|
8
|
+
|
|
9
|
+
await server.connect(transport);
|
|
10
|
+
Bun.serve({
|
|
11
|
+
port,
|
|
12
|
+
idleTimeout: 255,
|
|
13
|
+
fetch: (req) => {
|
|
14
|
+
if (new URL(req.url).pathname === "/mcp")
|
|
15
|
+
return transport.handleRequest(req);
|
|
16
|
+
return new Response("Not Found", { status: 404 });
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
console.log(`MCP Server: http://0.0.0.0:${port}/mcp`);
|
|
20
|
+
} else {
|
|
21
|
+
const { StdioServerTransport } =
|
|
22
|
+
await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { webSearch } from "./azure-openai.js";
|
|
4
|
+
import pkg from "../package.json" with { type: "json" };
|
|
5
|
+
|
|
6
|
+
export const server = new McpServer({
|
|
7
|
+
name: pkg.name,
|
|
8
|
+
version: pkg.version,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Output schema for structured content
|
|
12
|
+
const citationSchema = z.object({
|
|
13
|
+
title: z.string().describe("Title of the source"),
|
|
14
|
+
url: z.string().describe("URL of the source"),
|
|
15
|
+
start_index: z.number().describe("Start character index in text"),
|
|
16
|
+
end_index: z.number().describe("End character index in text"),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const searchResultSchema = z.object({
|
|
20
|
+
query: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Original search query (first item only)"),
|
|
24
|
+
text: z.string().describe("Search result text with inline citations"),
|
|
25
|
+
citations: z.array(citationSchema).describe("Source citations for the text"),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const outputSchema = z.object({
|
|
29
|
+
results: z.array(searchResultSchema),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
server.registerTool(
|
|
33
|
+
"web_search",
|
|
34
|
+
{
|
|
35
|
+
description:
|
|
36
|
+
"Search the web for real-time information. Returns results with source citations.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
query: z.string().describe("The search query"),
|
|
39
|
+
search_context_size: z
|
|
40
|
+
.enum(["low", "medium", "high"])
|
|
41
|
+
.default("medium")
|
|
42
|
+
.describe("Amount of search context"),
|
|
43
|
+
user_location: z
|
|
44
|
+
.object({
|
|
45
|
+
type: z.literal("approximate"),
|
|
46
|
+
city: z.string().nullish(),
|
|
47
|
+
country: z.string().nullish(),
|
|
48
|
+
region: z.string().nullish(),
|
|
49
|
+
timezone: z.string().nullish(),
|
|
50
|
+
})
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("User location for localized results"),
|
|
53
|
+
},
|
|
54
|
+
outputSchema,
|
|
55
|
+
},
|
|
56
|
+
async ({ query, search_context_size, user_location }) => {
|
|
57
|
+
try {
|
|
58
|
+
const response = await webSearch(
|
|
59
|
+
query,
|
|
60
|
+
search_context_size,
|
|
61
|
+
user_location,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Extract message content, keep structure but only useful fields
|
|
65
|
+
const message = response.output.find(
|
|
66
|
+
(item): item is Extract<typeof item, { type: "message" }> =>
|
|
67
|
+
item.type === "message",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const results = (message?.content ?? [])
|
|
71
|
+
.filter(
|
|
72
|
+
(c): c is Extract<typeof c, { type: "output_text" }> =>
|
|
73
|
+
c.type === "output_text",
|
|
74
|
+
)
|
|
75
|
+
.map(({ text, annotations }, index) => ({
|
|
76
|
+
...(index === 0 ? { query } : {}),
|
|
77
|
+
text,
|
|
78
|
+
citations: annotations
|
|
79
|
+
.filter(
|
|
80
|
+
(a): a is Extract<typeof a, { type: "url_citation" }> =>
|
|
81
|
+
a.type === "url_citation",
|
|
82
|
+
)
|
|
83
|
+
.map(({ title, url, start_index, end_index }) => ({
|
|
84
|
+
title,
|
|
85
|
+
url,
|
|
86
|
+
start_index,
|
|
87
|
+
end_index,
|
|
88
|
+
})),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: [],
|
|
93
|
+
structuredContent: { results },
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: `Error: ${String(error)}` }],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
);
|