desearch-mcp-server 0.1.2 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+
5
+ Tool copy no longer states unsourced speed or latency.
6
+
7
+ - `ai-search` description is `AI search and analysis on web using Desearch AI`.
8
+ - `x-search` description is `Search X (Twitter) using Desearch AI. Optional filters narrow by user, date, language, verification, media, and engagement. Sort stays Top.`
9
+ - `ai-search` `model` is `Model to use for the search: NOVA (default) or ORBIT.`
10
+
11
+ Removed from those strings: `Real-time`, `real-time`, and `Nova is 10s model, Orbit is 30s model`. The Vercel section of the README no longer says Orbit searches run about 30 seconds. The 60-second figure there is the `vercel.json` function duration.
12
+
3
13
  ## 0.1.2
4
14
 
5
15
  Migration from the published npm package `desearch-mcp-server@0.0.1`.
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/desearch-mcp-server.svg)](https://www.npmjs.com/package/desearch-mcp-server)
4
4
 
5
- A Model Context Protocol (MCP) server lets clients like Claude or Cursor use Desearch for real-time AI search, X search, web search, page extraction, and X trends.
5
+ AI search, X search and web search for AI agents, plus page extraction and X data tools. Bring your own Desearch API key.
6
6
 
7
7
  ## Tools
8
8
 
9
9
  The Desearch MCP server includes the following tools:
10
10
 
11
- - **AI Search** (`ai-search`): Performs real-time AI Twitter and web searches with relevant links and summary. `tools` uses short source ids (`web`, `twitter`, `arxiv`, `wikipedia`, `youtube`, `hackernews`, `reddit`). Older labels such as `Web Search` are still accepted and sent to the API as the short id. Default is `["web", "twitter"]`.
12
- - **X Search** (`x-search`): Real-time tweet search on X. Arguments: `query` (required), `count` (optional, default 20). Sort stays Top. Optional filters: `user`, `start_date`, `end_date` (YYYY-MM-DD), `lang`, `verified`, `blue_verified`, `is_quote`, `is_video`, `is_image`, `min_retweets`, `min_replies`, `min_likes`.
11
+ - **AI Search** (`ai-search`): Performs AI Twitter and web searches with relevant links and summary. `tools` uses short source ids (`web`, `twitter`, `arxiv`, `wikipedia`, `youtube`, `hackernews`, `reddit`). Older labels such as `Web Search` are still accepted and sent to the API as the short id. Default is `["web", "twitter"]`.
12
+ - **X Search** (`x-search`): Tweet search on X. Arguments: `query` (required), `count` (optional, default 20). Sort stays Top. Optional filters: `user`, `start_date`, `end_date` (YYYY-MM-DD), `lang`, `verified`, `blue_verified`, `is_quote`, `is_video`, `is_image`, `min_retweets`, `min_replies`, `min_likes`.
13
13
  - **Web Search** (`web-search`): SERP-style web search. Arguments: `query` (required), `start` (optional pagination offset).
14
14
  - **Web Links Search** (`web-links-search`): Web link search. Arguments: `prompt` (required), `tools` (optional, only `web`, default `["web"]`; `Web Search` is accepted and rewritten to `web`), `count` (optional, 10–200). The links/web API rejects other sources, so they are not in the enum.
15
15
  - **Extract** (`extract`): Read a public URL as text or HTML. Preferred over crawl. Arguments: `url` (required), `format` (optional, `html` or `text`), `js` (optional), `wait` (optional milliseconds).
@@ -69,18 +69,94 @@ Cursor or Claude can start that bin directly:
69
69
 
70
70
  ### Using Smithery
71
71
 
72
- To install the Desearch MCP server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@Desearch-ai/desearch):
72
+ To install the Desearch MCP server for Claude Desktop automatically via [Smithery](https://smithery.ai/servers/desearch/desearch):
73
73
 
74
74
  ```bash
75
- npx -y @smithery/cli install @Desearch-ai/desearch --client claude
75
+ npx -y @smithery/cli install desearch/desearch --client claude
76
76
  ```
77
77
 
78
78
  Or for Cursor IDE:
79
79
 
80
80
  ```bash
81
- npx -y @smithery/cli install @Desearch-ai/desearch --client cursor
81
+ npx -y @smithery/cli install desearch/desearch --client cursor
82
82
  ```
83
83
 
84
+ ### Windsurf
85
+
86
+ Windsurf's Cascade agent reads MCP servers from `mcp_config.json` under the `mcpServers` key. Open it from the Cascade panel: click the `...` (Actions) menu, then `Open MCP config file`. Windsurf builds use `~/.codeium/windsurf/mcp_config.json` (on Windows, `%USERPROFILE%\.codeium\windsurf\mcp_config.json`). Newer builds may open `~/.config/devin/mcp_config.json` instead (Windows: `%APPDATA%\devin\mcp_config.json`); edit whichever file that action opens.
87
+
88
+ Hosted server (no local install). Remote servers use `serverUrl` with `headers`:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "desearch": {
94
+ "serverUrl": "https://mcp.desearch.ai/mcp",
95
+ "headers": {
96
+ "x-api-key": "your-api-key"
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ To keep the key out of the file, Windsurf can interpolate an environment variable: `"x-api-key": "${env:DESEARCH_API_KEY}"`.
104
+
105
+ Local stdio alternative:
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "desearch": {
111
+ "command": "npx",
112
+ "args": ["-y", "desearch-mcp-server"],
113
+ "env": {
114
+ "DESEARCH_API_KEY": "your-api-key"
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ Save the file, then refresh the MCP servers list in Cascade.
122
+
123
+ ### Zed
124
+
125
+ Zed calls MCP servers context servers. Open your settings file with the `zed: open settings file` action (or use Settings → AI → MCP Servers → `Add Server`) and add a `context_servers` entry.
126
+
127
+ Hosted server:
128
+
129
+ ```json
130
+ {
131
+ "context_servers": {
132
+ "desearch": {
133
+ "url": "https://mcp.desearch.ai/mcp",
134
+ "headers": {
135
+ "x-api-key": "your-api-key"
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ Local stdio alternative:
143
+
144
+ ```json
145
+ {
146
+ "context_servers": {
147
+ "desearch": {
148
+ "command": "npx",
149
+ "args": ["-y", "desearch-mcp-server"],
150
+ "env": {
151
+ "DESEARCH_API_KEY": "your-api-key"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ The server is ready when the dot next to `desearch` in Settings → AI → MCP Servers turns green ("Server is active").
159
+
84
160
  ## Configuration ⚙️
85
161
 
86
162
  ### 1. Configure Cursor IDE to run the Desearch MCP server
@@ -164,18 +240,18 @@ For the changes to take effect:
164
240
 
165
241
  The same server can run over MCP Streamable HTTP for a remote client. Local stdio (`desearch-mcp-server`, Smithery) is unchanged and still reads `DESEARCH_API_KEY` from the environment.
166
242
 
167
- Remote requests do not use that environment variable. Each request must carry the caller's own Desearch API key, the same key from [console.desearch.ai/api-keys](https://console.desearch.ai/api-keys):
243
+ Remote requests do not use that environment variable. Discovery does not need a key: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list` return 200 so a marketplace scanner can read the tool list. `tools/call` and every other method still require the caller's own Desearch API key, the same key from [console.desearch.ai/api-keys](https://console.desearch.ai/api-keys):
168
244
 
169
245
  - `Authorization: Bearer <DESEARCH_API_KEY>` (preferred)
170
246
  - `x-api-key: <DESEARCH_API_KEY>`
171
247
 
172
- A bare `Authorization: <DESEARCH_API_KEY>` value is also accepted. The key is not read from the query string. There is no shared server secret: the hosted process forwards the per-request key to the Desearch API.
248
+ A bare `Authorization: <DESEARCH_API_KEY>` value is also accepted. The key is not read from the query string. There is no shared server secret and no `WWW-Authenticate` challenge: the hosted process forwards the per-request key to the Desearch API only when a call needs it.
173
249
 
174
- The MCP endpoint is `POST /mcp`. Responses are JSON (stateless Streamable HTTP). `GET` and `DELETE` on `/mcp` return `405` because the server does not keep a session or push server-to-client messages. `GET /` and `GET /health` are unauthenticated health checks.
250
+ The MCP endpoint is `POST /mcp`. Responses are JSON (stateless Streamable HTTP). `GET` and `DELETE` on `/mcp` return `405` because the server does not keep a session or push server-to-client messages. `GET /`, `GET /health`, and `GET /api/health` are unauthenticated health checks.
175
251
 
176
252
  ## Hosted endpoint
177
253
 
178
- The public Streamable HTTP endpoint is `https://mcp.desearch.ai/mcp`. Send your Desearch API key on each request in the `x-api-key` header. `Authorization: Bearer <key>` is also accepted. Use the key from [console.desearch.ai/api-keys](https://console.desearch.ai/api-keys). The server does not read a key from the query string. Remote requests do not use a process-level `DESEARCH_API_KEY`.
254
+ The public Streamable HTTP endpoint is `https://mcp.desearch.ai/mcp`. Listing the tools does not need a key. Send your Desearch API key on each `tools/call` in the `x-api-key` header. `Authorization: Bearer <key>` is also accepted. Use the key from [console.desearch.ai/api-keys](https://console.desearch.ai/api-keys). The server does not read a key from the query string. Remote requests do not use a process-level `DESEARCH_API_KEY`.
179
255
 
180
256
  Cursor, or any remote MCP client:
181
257
 
@@ -265,7 +341,7 @@ docker run --rm -e MCP_TRANSPORT=http -p 3000:3000 desearch-mcp
265
341
 
266
342
  ### Deploy on Vercel
267
343
 
268
- Vercel fits this server because the handler is stateless and answers each JSON-RPC call in one response. `vercel.json` builds the project, serves `POST /mcp`, and allows tool calls up to 60 seconds (Orbit searches run about 30 seconds). Hobby plans cap function duration lower than that, so AI Search tool calls need a plan that allows at least 60 seconds. `initialize` and `tools/list` are short either way.
344
+ Vercel fits this server because the handler is stateless and answers each JSON-RPC call in one response. `vercel.json` builds the project, serves `POST /mcp`, and sets the function duration to 60 seconds. Hobby plans cap function duration lower than that, so AI Search tool calls need a plan that allows at least 60 seconds. `initialize` and `tools/list` are short either way.
269
345
 
270
346
  No server-side Desearch API key is required in the Vercel project. After deploy, the endpoint is:
271
347
 
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../http.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,iBAAiB;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa/E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAkB1E;AAoED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyD9E;AAoDD,wBAAgB,eAAe,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiD3F"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../http.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,iBAAiB;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa/E;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAkB1E;AAkID;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiE9E;AAuDD,wBAAgB,eAAe,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiD3F"}
package/build/http.js CHANGED
@@ -68,7 +68,10 @@ function isMcpPath(pathname) {
68
68
  return pathname === "/mcp" || pathname === "/api/mcp";
69
69
  }
70
70
  function isHealthPath(pathname) {
71
- return pathname === "/" || pathname === "/health";
71
+ // `/api/health` is the Vercel function path. The public rewrite of `/health`
72
+ // delivers that pathname when the function wrapper does not remap it, and
73
+ // `isMcpPath` already accepts both `/mcp` and `/api/mcp` for the same reason.
74
+ return pathname === "/" || pathname === "/health" || pathname === "/api/health";
72
75
  }
73
76
  function withCors(response) {
74
77
  const headers = new Headers(response.headers);
@@ -96,7 +99,7 @@ function healthResponse() {
96
99
  version: SERVER_VERSION,
97
100
  transport: "streamable-http",
98
101
  endpoint: "/mcp",
99
- auth: "Authorization: Bearer <DESEARCH_API_KEY> or x-api-key: <DESEARCH_API_KEY>",
102
+ auth: "initialize and tools/list are public. tools/call requires Authorization: Bearer <DESEARCH_API_KEY> or x-api-key.",
100
103
  }), {
101
104
  status: 200,
102
105
  headers: { "Content-Type": "application/json" },
@@ -107,6 +110,62 @@ function unauthorized() {
107
110
  // WWW-Authenticate discovery hint makes some MCP clients start an OAuth flow.
108
111
  return jsonRpcError(401, -32001, "Unauthorized. Send your Desearch API key in the Authorization: Bearer <key> header or the x-api-key header.");
109
112
  }
113
+ /**
114
+ * Methods a marketplace scanner can call with no Desearch API key.
115
+ * Anything else, including tools/call, stays on the 401 gate so a missing
116
+ * key never reaches the Desearch API.
117
+ */
118
+ const KEYLESS_METHODS = new Set([
119
+ "initialize",
120
+ "notifications/initialized",
121
+ "ping",
122
+ "tools/list",
123
+ "prompts/list",
124
+ "resources/list",
125
+ "resources/templates/list",
126
+ ]);
127
+ function isKeylessDiscovery(raw) {
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(raw);
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
136
+ return (messages.length > 0 &&
137
+ messages.every((message) => {
138
+ if (typeof message !== "object" || message === null) {
139
+ return false;
140
+ }
141
+ const method = message.method;
142
+ return typeof method === "string" && KEYLESS_METHODS.has(method);
143
+ }));
144
+ }
145
+ function copyRequestHeaders(headers) {
146
+ const copied = new Headers();
147
+ headers.forEach((value, key) => {
148
+ if (key.toLowerCase() === "content-length" || key.toLowerCase() === "host") {
149
+ return;
150
+ }
151
+ try {
152
+ copied.append(key, value);
153
+ }
154
+ catch {
155
+ // The Fetch constructor rejects forbidden request headers such as host.
156
+ }
157
+ });
158
+ return copied;
159
+ }
160
+ function requestWithJsonBody(request, body) {
161
+ const init = { method: "POST", headers: request.headers, body };
162
+ try {
163
+ return new Request(request.url, init);
164
+ }
165
+ catch {
166
+ return new Request(request.url, { ...init, headers: copyRequestHeaders(request.headers) });
167
+ }
168
+ }
110
169
  /**
111
170
  * Stateless Streamable HTTP handler.
112
171
  * Each POST builds a fresh MCP server tied to that request's API key.
@@ -131,16 +190,24 @@ export async function handleMcpHttpRequest(request) {
131
190
  headers: { "Content-Type": "application/json" },
132
191
  }));
133
192
  }
134
- const apiKey = extractDesearchApiKey(request);
135
- if (!apiKey) {
136
- return withCors(unauthorized());
137
- }
138
193
  if (request.method === "GET" || request.method === "DELETE") {
194
+ if (!extractDesearchApiKey(request)) {
195
+ return withCors(unauthorized());
196
+ }
139
197
  return withCors(jsonRpcError(405, -32000, "Method not allowed. This server is stateless; use POST.", { Allow: "POST" }));
140
198
  }
141
199
  if (request.method !== "POST") {
142
200
  return withCors(jsonRpcError(405, -32000, "Method not allowed.", { Allow: "POST" }));
143
201
  }
202
+ let apiKey = extractDesearchApiKey(request);
203
+ if (!apiKey) {
204
+ const raw = await request.text();
205
+ if (!isKeylessDiscovery(raw)) {
206
+ return withCors(unauthorized());
207
+ }
208
+ request = requestWithJsonBody(request, raw);
209
+ apiKey = "";
210
+ }
144
211
  const server = createDesearchMcpServer(apiKey);
145
212
  const transport = new WebStandardStreamableHTTPServerTransport({
146
213
  enableJsonResponse: true,
@@ -168,7 +235,10 @@ function readRawBody(req) {
168
235
  req.on("data", (chunk) => {
169
236
  chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
170
237
  });
171
- req.on("end", () => resolve(Buffer.concat(chunks)));
238
+ // TS 5.7+ DOM BodyInit accepts Uint8Array<ArrayBuffer> and rejects
239
+ // Uint8Array<ArrayBufferLike> (Node Buffer, whose buffer may be a
240
+ // SharedArrayBuffer). Copy into a plain ArrayBuffer.
241
+ req.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
172
242
  req.on("error", reject);
173
243
  });
174
244
  }
package/build/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  export declare const SERVER_NAME = "Desearch";
3
- export declare const SERVER_VERSION = "0.1.2";
3
+ export declare const SERVER_VERSION = "0.1.3";
4
4
  interface XSearchPayload {
5
5
  query: string;
6
6
  sort?: "Top" | "Latest";
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"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"}
package/build/server.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
3
  import DesearchImport from "desearch-js";
3
4
  import { z } from "zod";
4
5
  import { AI_SEARCH_TOOLS, WEB_LINK_TOOLS, toolIdSchema } from "./tool-sources.js";
5
6
  export const SERVER_NAME = "Desearch";
6
- export const SERVER_VERSION = "0.1.2";
7
+ export const SERVER_VERSION = "0.1.3";
7
8
  // desearch-js 1.5 publishes a default class. Node16 resolution types that package
8
9
  // as a module namespace, so the constructor is applied through a cast.
9
10
  const Desearch = DesearchImport;
@@ -135,11 +136,22 @@ function fail(label, error) {
135
136
  isError: true,
136
137
  };
137
138
  }
138
- function registerTool(server, name, description, inputSchema, handler) {
139
+ function registerTool(server, apiKey, name, title, description, inputSchema, handler) {
139
140
  // SDK 1.30's tool generics blow the TypeScript instantiation limit on these
140
141
  // schemas. Runtime registration is the same registerTool call.
141
142
  const register = server.registerTool.bind(server);
142
- register(name, { description, inputSchema, annotations: READ_ONLY_OPEN_WORLD }, handler);
143
+ const guarded = async (args) => {
144
+ if (!apiKey) {
145
+ return fail("Desearch", new Error("Desearch API key required. Send it in the Authorization: Bearer <key> header or the x-api-key header."));
146
+ }
147
+ return handler(args);
148
+ };
149
+ register(name, {
150
+ title,
151
+ description,
152
+ inputSchema,
153
+ annotations: { ...READ_ONLY_OPEN_WORLD, title },
154
+ }, guarded);
143
155
  }
144
156
  /**
145
157
  * One MCP server bound to a single Desearch API key.
@@ -154,7 +166,7 @@ export function createDesearchMcpServer(apiKey, client) {
154
166
  name: SERVER_NAME,
155
167
  version: SERVER_VERSION,
156
168
  });
157
- registerTool(server, "ai-search", "Real-time AI search and analysis on web using Desearch AI", {
169
+ registerTool(server, apiKey, "ai-search", "AI Search", "AI search and analysis on web using Desearch AI", {
158
170
  prompt: z.string().describe("Question, example: 'What is the latest news on AI?'"),
159
171
  tools: z
160
172
  .array(toolIdSchema(AI_SEARCH_TOOLS))
@@ -197,7 +209,7 @@ export function createDesearchMcpServer(apiKey, client) {
197
209
  model: z
198
210
  .enum(["NOVA", "ORBIT"])
199
211
  .default("NOVA")
200
- .describe("Model to use for the search, example: 'NOVA', Nova is 10s model, Orbit is 30s model"),
212
+ .describe("Model to use for the search: NOVA (default) or ORBIT."),
201
213
  }, async ({ prompt, tools, date_filter, start_date, end_date, result_type, include_domains, exclude_domains, model }) => {
202
214
  try {
203
215
  const payload = {
@@ -218,7 +230,7 @@ export function createDesearchMcpServer(apiKey, client) {
218
230
  return fail("AI Search error", error);
219
231
  }
220
232
  });
221
- registerTool(server, "x-search", "Search the X (Twitter) using Desearch AI - performs real-time tweet search on X. Optional filters narrow by user, date, language, verification, media, and engagement. Sort stays Top.", {
233
+ registerTool(server, apiKey, "x-search", "X Search", "Search X (Twitter) using Desearch AI. Optional filters narrow by user, date, language, verification, media, and engagement. Sort stays Top.", {
222
234
  query: z
223
235
  .string()
224
236
  .describe("Twitter advanced search query, example: 'from:elonmusk since:2023-01-01 min_replies:10'"),
@@ -271,7 +283,7 @@ export function createDesearchMcpServer(apiKey, client) {
271
283
  return fail("X Search error", error);
272
284
  }
273
285
  });
274
- registerTool(server, "web-search", "SERP-style web search using Desearch. Returns ranked titles, links, and snippets.", {
286
+ registerTool(server, apiKey, "web-search", "Web Search", "SERP-style web search using Desearch. Returns ranked titles, links, and snippets.", {
275
287
  query: z.string().describe("Search query, example: 'latest news on AI'"),
276
288
  start: z
277
289
  .number()
@@ -287,7 +299,7 @@ export function createDesearchMcpServer(apiKey, client) {
287
299
  return fail("Web Search error", error);
288
300
  }
289
301
  });
290
- registerTool(server, "web-links-search", "Search the web for links using Desearch. Only the web source is accepted.", {
302
+ registerTool(server, apiKey, "web-links-search", "Web Links Search", "Search the web for links using Desearch. Only the web source is accepted.", {
291
303
  prompt: z
292
304
  .string()
293
305
  .describe("Search query prompt, example: 'open source browser automation tools'"),
@@ -315,7 +327,7 @@ export function createDesearchMcpServer(apiKey, client) {
315
327
  return fail("Web Links Search error", error);
316
328
  }
317
329
  });
318
- registerTool(server, "x-links-search", "AI search for X (Twitter) post links using Desearch. Returns links from posts that match the prompt.", {
330
+ registerTool(server, apiKey, "x-links-search", "X Links Search", "AI search for X (Twitter) post links using Desearch. Returns links from posts that match the prompt.", {
319
331
  prompt: z.string().describe("Search query prompt, example: 'Bittensor subnet updates'"),
320
332
  count: optionalLinksCount,
321
333
  }, async ({ prompt, count }) => {
@@ -326,7 +338,7 @@ export function createDesearchMcpServer(apiKey, client) {
326
338
  return fail("X Links Search error", error);
327
339
  }
328
340
  });
329
- registerTool(server, "x-posts-by-urls", "Fetch full X (Twitter) posts for a list of post URLs.", {
341
+ registerTool(server, apiKey, "x-posts-by-urls", "Get X Posts by URLs", "Fetch full X (Twitter) posts for a list of post URLs.", {
330
342
  urls: z
331
343
  .array(z.string())
332
344
  .min(1)
@@ -339,7 +351,7 @@ export function createDesearchMcpServer(apiKey, client) {
339
351
  return fail("X Posts By URLs error", error);
340
352
  }
341
353
  });
342
- registerTool(server, "x-post-by-id", "Fetch a single X (Twitter) post by its ID.", {
354
+ registerTool(server, apiKey, "x-post-by-id", "Get X Post by ID", "Fetch a single X (Twitter) post by its ID.", {
343
355
  id: z.string().describe("The unique ID of the post, example: '1234567890'"),
344
356
  }, async ({ id }) => {
345
357
  try {
@@ -349,7 +361,7 @@ export function createDesearchMcpServer(apiKey, client) {
349
361
  return fail("X Post By ID error", error);
350
362
  }
351
363
  });
352
- registerTool(server, "x-posts-by-user", "Search X (Twitter) posts by a specific user, with an optional keyword query.", {
364
+ registerTool(server, apiKey, "x-posts-by-user", "Search X Posts by User", "Search X (Twitter) posts by a specific user, with an optional keyword query.", {
353
365
  user: z.string().describe("User to search for, example: 'elonmusk'"),
354
366
  query: z.string().optional().describe("Advanced search query to filter this user's posts."),
355
367
  count: optionalPostCount,
@@ -361,7 +373,7 @@ export function createDesearchMcpServer(apiKey, client) {
361
373
  return fail("X Posts By User error", error);
362
374
  }
363
375
  });
364
- registerTool(server, "x-post-retweeters", "List users who retweeted an X (Twitter) post. Pass cursor to page through more users.", {
376
+ registerTool(server, apiKey, "x-post-retweeters", "List X Post Retweeters", "List users who retweeted an X (Twitter) post. Pass cursor to page through more users.", {
365
377
  id: z.string().describe("The ID of the post to get retweeters for."),
366
378
  cursor: z.string().optional().describe("Cursor for pagination from a previous response."),
367
379
  }, async ({ id, cursor }) => {
@@ -372,7 +384,7 @@ export function createDesearchMcpServer(apiKey, client) {
372
384
  return fail("X Post Retweeters error", error);
373
385
  }
374
386
  });
375
- registerTool(server, "x-user-posts", "Retrieve a user's X (Twitter) timeline posts by username. Pass cursor to page through more posts.", {
387
+ registerTool(server, apiKey, "x-user-posts", "Get X User Timeline", "Retrieve a user's X (Twitter) timeline posts by username. Pass cursor to page through more posts.", {
376
388
  username: z.string().describe("Username to fetch posts for, example: 'elonmusk'"),
377
389
  cursor: z.string().optional().describe("Cursor for pagination from a previous response."),
378
390
  }, async ({ username, cursor }) => {
@@ -383,7 +395,7 @@ export function createDesearchMcpServer(apiKey, client) {
383
395
  return fail("X User Posts error", error);
384
396
  }
385
397
  });
386
- registerTool(server, "x-user-replies", "Fetch posts and replies by an X (Twitter) user, with an optional keyword query.", {
398
+ registerTool(server, apiKey, "x-user-replies", "Get X User Replies", "Fetch posts and replies by an X (Twitter) user, with an optional keyword query.", {
387
399
  user: z.string().describe("Username of the user to search for, example: 'elonmusk'"),
388
400
  count: optionalPostCount,
389
401
  query: z.string().optional().describe("Advanced search query to filter this user's posts and replies."),
@@ -395,7 +407,7 @@ export function createDesearchMcpServer(apiKey, client) {
395
407
  return fail("X User Replies error", error);
396
408
  }
397
409
  });
398
- registerTool(server, "x-post-replies", "Fetch replies to an X (Twitter) post, with an optional keyword query.", {
410
+ registerTool(server, apiKey, "x-post-replies", "Get X Post Replies", "Fetch replies to an X (Twitter) post, with an optional keyword query.", {
399
411
  post_id: z.string().describe("The ID of the post to fetch replies for."),
400
412
  count: optionalPostCount,
401
413
  query: z.string().optional().describe("Advanced search query to filter replies."),
@@ -407,7 +419,7 @@ export function createDesearchMcpServer(apiKey, client) {
407
419
  return fail("X Post Replies error", error);
408
420
  }
409
421
  });
410
- registerTool(server, "extract", "Extract a public URL and return its content as plain text or HTML using Desearch. Preferred over web-crawl for new integrations.", pageContentFields(), async ({ url, format, js, wait }) => {
422
+ registerTool(server, apiKey, "extract", "Extract Page Content", "Extract a public URL and return its content as plain text or HTML using Desearch. Preferred over web-crawl for new integrations.", pageContentFields(), async ({ url, format, js, wait }) => {
411
423
  try {
412
424
  return ok(await desearch.extract({ url, format, js, wait }));
413
425
  }
@@ -415,7 +427,7 @@ export function createDesearchMcpServer(apiKey, client) {
415
427
  return fail("Extract error", error);
416
428
  }
417
429
  });
418
- registerTool(server, "web-crawl", "Crawl a public URL and return its content as plain text or HTML on the legacy Desearch /web/crawl route. The SDK marks webCrawl deprecated in favor of extract; this tool stays for parity with that route. Prefer extract for new integrations.", pageContentFields(), async ({ url, format, js, wait }) => {
430
+ registerTool(server, apiKey, "web-crawl", "Crawl Web Page (Legacy)", "Crawl a public URL and return its content as plain text or HTML on the legacy Desearch /web/crawl route. The SDK marks webCrawl deprecated in favor of extract; this tool stays for parity with that route. Prefer extract for new integrations.", pageContentFields(), async ({ url, format, js, wait }) => {
419
431
  try {
420
432
  return ok(await desearch.webCrawl({ url, format, js, wait }));
421
433
  }
@@ -423,7 +435,7 @@ export function createDesearchMcpServer(apiKey, client) {
423
435
  return fail("Web Crawl error", error);
424
436
  }
425
437
  });
426
- registerTool(server, "x-trends", "Retrieve trending topics on X (Twitter) for a location by its WOEID using Desearch.", {
438
+ registerTool(server, apiKey, "x-trends", "Get X Trends", "Retrieve trending topics on X (Twitter) for a location by its WOEID using Desearch.", {
427
439
  woeid: z
428
440
  .number()
429
441
  .int()
@@ -443,5 +455,17 @@ export function createDesearchMcpServer(apiKey, client) {
443
455
  return fail("X Trends error", error);
444
456
  }
445
457
  });
458
+ // Scanners ask for these lists during discovery. There is nothing to return,
459
+ // but an empty list is a successful response. Method-not-found looks like a
460
+ // broken server to some directory crawlers.
461
+ server.server.registerCapabilities({
462
+ prompts: {},
463
+ resources: {},
464
+ });
465
+ server.server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: [] }));
466
+ server.server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
467
+ server.server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({
468
+ resourceTemplates: [],
469
+ }));
446
470
  return server;
447
471
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "desearch-mcp-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "mcpName": "io.github.Desearch-ai/mcp-desearch",
5
- "description": "A Model Context Protocol server with Desearch for real-time AI search, X search and web search.",
5
+ "description": "AI search, X search and web search for AI agents, plus page extraction and X data tools. Bring your own Desearch API key.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://www.desearch.ai/docs/guide/sdk/mcp",
8
8
  "type": "module",