@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 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.0.1",
4
- "license": "MIT",
5
- "publishConfig": {
6
- "access": "public"
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
+ );