mcp-www 0.1.4 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +65 -64
  2. package/dist/index.js +356 -210
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
7
 
8
- **DNS-based MCP service discovery over UDP.**
8
+ **DNS-based MCP service discovery and installation.**
9
9
 
10
10
  ## Problem
11
11
 
@@ -15,12 +15,12 @@ Agents need to discover MCP servers, but current approaches lean on centralized
15
15
 
16
16
  **mcp-www** is itself a standard MCP server. An agent connects to it the same way it connects to any other MCP server — no new client code, no special SDK, no registry signup.
17
17
 
18
- Once connected, the agent calls the `browse_domain` tool with a domain name. mcp-www performs a standard **UDP DNS TXT lookup** for `_mcp.{domain}`, parses the semicolon-delimited record, and returns structured metadata about the MCP servers published by that domain.
18
+ Once connected, the agent calls `discover` with a domain name. mcp-www performs a standard **UDP DNS TXT lookup** for `_mcp.{domain}`, parses the records, and returns all advertised MCP servers. Then `browse` connects to those servers and retrieves their full manifests. Finally, `install` generates the config to permanently add a server to the user's MCP client.
19
19
 
20
20
  ```
21
- Agent → mcp-www (MCP server) → UDP DNS query for _mcp.example.com TXT
22
- "v=mcp1; src=https://mcp.example.com; ..."
23
- Structured JSON response
21
+ Agent → mcp-www → discover("example.com") → DNS TXT lookup
22
+ browse("example.com") → server card GET with MCP handshake fallback
23
+ install("example.com") → config for Claude Desktop / VS Code / Cursor / Windsurf
24
24
  ```
25
25
 
26
26
  No HTTP registry in the loop. The DNS infrastructure **is** the registry.
@@ -55,92 +55,62 @@ Add to your MCP client config (e.g., `.mcp.json`):
55
55
 
56
56
  ## Try It
57
57
 
58
- 🤖 Use your agent for easy `mcp-www` installation or follow the manual steps above. Simply ask it to install mcp-www from npm (many clients require a restart for a new server.)
59
-
60
- 🚀 **Publish your own MCP server via _dns TXT record** - or -
61
-
62
58
  **[korm.co](https://korm.co)** publishes a live `_mcp` TXT record. You can discover and interact with it end-to-end:
63
59
 
64
60
  ```
65
- browse_discover("korm.co") discovers server, returns tools + resources + prompts
66
- browse_server("https://mcp.korm.co") inspects tools, resources, and prompts
67
- call_remote_tool("https://mcp.korm.co", "list_articles") → returns blog articles
68
- read_remote_resource("https://mcp.korm.co", "korm://bio") → reads author bio
69
- get_remote_prompt("https://mcp.korm.co", "recommend-post", { "topic": "AI" }) → gets prompt
61
+ discover("korm.co") DNS lookup, returns all _mcp TXT records
62
+ discover_browse("korm.co") DNS + server card in one call, init as fallback
63
+ browse({ domain: "korm.co" }) → server card first, MCP handshake as fallback
64
+ call_remote_tool("https://mcp.korm.co", "browse_posts") → returns blog articles
65
+ read_remote_resource("https://mcp.korm.co", "korm://bio") reads author bio
66
+ get_remote_prompt("https://mcp.korm.co", "recommend-post", { "topic": "AI" }) → gets prompt
67
+ install({ domain: "korm.co" }) → generates config to add to your MCP client
70
68
  ```
71
- ⚠️ korm.co is a construction zone of new services so pardon any dust or temporary infra outages, thanks for your patience.
72
-
73
69
 
74
70
  ## Key Design Points
75
71
 
76
- - **Uses UDP DNS (port 53) for lookups** — the lightest possible network primitive. No TCP handshake, no TLS negotiation, no HTTP overhead. A single UDP packet out, a single packet back.
72
+ - **Uses UDP DNS (port 53) for lookups** — the lightest possible network primitive. A single UDP packet out, a single packet back.
77
73
  - **The DNS infrastructure IS the registry** — no additional servers to deploy, no uptime to maintain, no accounts to create. If you can publish a TXT record, you can advertise your MCP server.
78
- - **mcp-www is a standard MCP server** — any MCP-compliant agent can use it with zero new client code. It's just another server in your agent's config.
79
- - **Supports the `_mcp` TXT record convention** — records follow a semicolon-delimited format:
74
+ - **mcp-www is a standard MCP server** — any MCP-compliant agent can use it with zero new client code.
75
+ - **Multiple TXT records supported** — a domain can advertise multiple MCP servers via separate `_mcp` TXT records.
76
+ - **Supports the `_mcp` TXT record convention:**
80
77
  ```
81
78
  v=mcp1; src=https://mcp.example.com; auth=oauth2
82
79
  ```
83
- - **Works with split-horizon DNS** — enterprise and private networks can publish internal `_mcp` records visible only inside their network, enabling private service discovery without exposing anything to the public internet.
80
+ - **Works with split-horizon DNS** — enterprise and private networks can publish internal `_mcp` records visible only inside their network.
81
+ - Allows overriding the default system DNS resolver via environment variable: `MCP_DNS_SERVER=192.168.68.133:5335 npx mcp-www`
84
82
 
85
- ## Tools Exposed
83
+ ## Tools
86
84
 
87
- ### `browse_domain`
85
+ ### `discover`
88
86
 
89
- Lookup `_mcp.{domain}` TXT records and return a parsed list of discovered MCP servers.
87
+ DNS-only lookup. Returns all `_mcp.{domain}` TXT records there can be multiple, each advertising a different MCP server. Supports single domain or batch lookup.
90
88
 
91
89
  ```json
92
- {
93
- "tool": "browse_domain",
94
- "arguments": {
95
- "domain": "example.com"
96
- }
97
- }
90
+ { "tool": "discover", "arguments": { "domain": "example.com" } }
91
+ { "tool": "discover", "arguments": { "domains": ["example.com", "acme.org"] } }
98
92
  ```
99
93
 
100
- Returns structured server metadata: server URL, protocol version, auth requirements, and any additional fields published in the TXT record.
101
-
102
- ### `browse_discover`
94
+ ### `discover_browse`
103
95
 
104
- Discover and inspect in one step. Looks up `_mcp.{domain}` TXT records, connects to the advertised server URL, and retrieves its full manifest tools, resources, and prompts.
96
+ DNS lookup + server card in one call. Looks up all `_mcp.{domain}` TXT records, then fetches `.well-known/mcp.json` for server metadata. Only falls back to MCP initialize if no server card is found.
105
97
 
106
98
  ```json
107
- {
108
- "tool": "browse_discover",
109
- "arguments": {
110
- "domain": "example.com"
111
- }
112
- }
113
- ```
114
-
115
- ### `browse_server`
116
-
117
- Given a discovered server URL, connect to it and retrieve its full manifest: tools, resources, and prompts. Lets the agent inspect what a discovered server actually offers before deciding to connect.
118
-
119
- ```json
120
- {
121
- "tool": "browse_server",
122
- "arguments": {
123
- "url": "https://mcp.example.com"
124
- }
125
- }
99
+ { "tool": "discover_browse", "arguments": { "domain": "example.com" } }
126
100
  ```
127
101
 
128
- ### `browse_multi`
102
+ ### `browse`
129
103
 
130
- Batch lookup across multiple domains in a single call. Useful for scanning a list of known domains or performing broad discovery.
104
+ Inspect a domain or server URL. Tries `.well-known/mcp.json` (server card) first, only falls back to MCP initialize handshake if no server card is found. For domains: also performs DNS lookup for `_mcp` TXT records.
131
105
 
132
106
  ```json
133
- {
134
- "tool": "browse_multi",
135
- "arguments": {
136
- "domains": ["example.com", "acme.org", "internal.corp"]
137
- }
138
- }
107
+ { "tool": "browse", "arguments": { "domain": "example.com" } }
108
+ { "tool": "browse", "arguments": { "url": "https://mcp.example.com" } }
139
109
  ```
140
110
 
141
111
  ### `call_remote_tool`
142
112
 
143
- Call a tool on a remote MCP server. Use `browse_server` first to discover available tools, then use this to execute them. Handles the JSON-RPC initialize handshake and `tools/call` request.
113
+ Call a tool on a remote MCP server. Use `browse` first to discover available tools, then use this to execute them.
144
114
 
145
115
  ```json
146
116
  {
@@ -155,7 +125,7 @@ Call a tool on a remote MCP server. Use `browse_server` first to discover availa
155
125
 
156
126
  ### `read_remote_resource`
157
127
 
158
- Read a resource from a remote MCP server. Use `browse_server` or `browse_discover` first to see available resources, then use this to read one by its URI.
128
+ Read a resource from a remote MCP server by its URI.
159
129
 
160
130
  ```json
161
131
  {
@@ -169,7 +139,7 @@ Read a resource from a remote MCP server. Use `browse_server` or `browse_discove
169
139
 
170
140
  ### `get_remote_prompt`
171
141
 
172
- Get a prompt from a remote MCP server. Use `browse_server` or `browse_discover` first to see available prompts, then use this to retrieve one with optional arguments.
142
+ Get a prompt from a remote MCP server with optional arguments.
173
143
 
174
144
  ```json
175
145
  {
@@ -182,13 +152,44 @@ Get a prompt from a remote MCP server. Use `browse_server` or `browse_discover`
182
152
  }
183
153
  ```
184
154
 
155
+ ### `install`
156
+
157
+ Generate client configuration to permanently add a discovered MCP server. Returns config file paths and JSON entries for Claude Desktop, VS Code, Cursor, and Windsurf. The agent reads the target config file, merges the entry, and writes it back.
158
+
159
+ ```json
160
+ { "tool": "install", "arguments": { "domain": "example.com" } }
161
+ { "tool": "install", "arguments": { "url": "https://mcp.example.com", "name": "my-server" } }
162
+ ```
185
163
 
186
164
  ## Status
187
165
 
188
- **Working.** The server implements DNS-based discovery, server inspection, remote tool calling, resource reading, and prompt retrieval over the Streamable HTTP transport.
166
+ **Working.** The server implements DNS-based discovery, server inspection, remote tool calling, resource reading, prompt retrieval, and client installation over the Streamable HTTP transport.
189
167
 
190
168
  Feedback, criticism, and alternative approaches are welcome — open an issue or start a discussion.
191
169
 
170
+ ## Security
171
+
172
+ ### DNS-based trust model
173
+
174
+ Because mcp-www uses DNS TXT records for discovery, domain ownership is enforced by DNS infrastructure itself — only the domain owner (or their DNS provider) can publish `_mcp` TXT records. This is inherently stronger than centralized registries, which introduce a single point of compromise.
175
+
176
+ ### IDN homograph attack detection
177
+
178
+ mcp-www detects [IDN homograph attacks](https://en.wikipedia.org/wiki/IDN_homograph_attack) on all domain lookups. These attacks use visually identical characters from different Unicode scripts (e.g., Cyrillic "a" vs Latin "a") to spoof legitimate domains.
179
+
180
+ Detection covers:
181
+ - **Punycode-encoded domains** — labels starting with `xn--` (the ASCII encoding of internationalized domain names)
182
+ - **Mixed-script labels** — a single label containing characters from multiple scripts (e.g., Latin + Cyrillic)
183
+ - **Non-Latin labels** — fully Cyrillic/Greek labels that could visually mimic common Latin domains
184
+
185
+ When detected, a prominent warning is surfaced as a separate content block, instructing the agent to verify the domain with the user before proceeding. Lookups are not blocked — the warning is informational.
186
+
187
+ ### Additional considerations
188
+
189
+ - **No implicit trust** — mcp-www discovers and inspects remote servers, but tool execution (`call_remote_tool`) is always an explicit agent action.
190
+ - **Split-horizon DNS** — private/internal `_mcp` records are only resolvable within the network they're published on.
191
+ - **Unicode normalization** — all domain inputs are NFC-normalized before lookup.
192
+
192
193
  ## Related
193
194
 
194
195
  - [Model Context Protocol Specification](https://modelcontextprotocol.io/specification)
package/dist/index.js CHANGED
@@ -15,6 +15,11 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
15
15
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
16
16
  const node_dns_1 = __importDefault(require("node:dns"));
17
17
  const node_util_1 = require("node:util");
18
+ // Allow overriding the DNS resolver via environment variable.
19
+ // Supports "host:port" format (e.g., "192.168.68.133:5335").
20
+ if (process.env.MCP_DNS_SERVER) {
21
+ node_dns_1.default.setServers([process.env.MCP_DNS_SERVER]);
22
+ }
18
23
  const resolveTxt = (0, node_util_1.promisify)(node_dns_1.default.resolveTxt);
19
24
  // --- IDN Homograph Detection ---
20
25
  function detectHomograph(domain) {
@@ -25,7 +30,6 @@ function detectHomograph(domain) {
25
30
  return `Domain contains punycode-encoded label(s): ${punycodeLabels.join(", ")}. This may be an IDN homograph attack — visually similar characters from different scripts (e.g., Cyrillic) can make a domain look identical to a legitimate one.`;
26
31
  }
27
32
  // Check for mixed Unicode scripts within a single label
28
- // Common confusable scripts: Cyrillic, Greek, Latin
29
33
  for (const label of labels) {
30
34
  let hasLatin = false;
31
35
  let hasNonLatin = false;
@@ -45,14 +49,10 @@ function detectHomograph(domain) {
45
49
  if (!detectedScripts.includes("Greek"))
46
50
  detectedScripts.push("Greek");
47
51
  }
48
- else if (code >= 0x0100 && code <= 0x024f) {
49
- // Latin Extended — not suspicious on its own but flag with others
50
- }
51
52
  }
52
53
  if (hasLatin && hasNonLatin) {
53
54
  return `Domain label "${label}" mixes Latin with ${detectedScripts.join("/")} characters. This is a strong indicator of an IDN homograph attack.`;
54
55
  }
55
- // Fully non-Latin label (e.g., all Cyrillic) pretending to be a common domain
56
56
  if (hasNonLatin && !hasLatin && detectedScripts.length > 0) {
57
57
  return `Domain label "${label}" uses ${detectedScripts.join("/")} script characters that may visually mimic Latin characters. Verify this is the intended domain.`;
58
58
  }
@@ -72,49 +72,44 @@ function formatHomographWarning(warning) {
72
72
  };
73
73
  }
74
74
  // --- TXT Record Parser ---
75
- function parseMcpTxtRecord(txtRecords) {
76
- // TXT records come as arrays of strings (chunked), join them
77
- const fullRecord = txtRecords.map((chunks) => chunks.join("")).join("");
75
+ function parseSingleTxtRecord(chunks) {
76
+ const fullRecord = chunks.join("");
78
77
  const result = {};
79
- // Parse semicolon-delimited key=value pairs
80
- // e.g. "v=mcp1; endpoint=https://...; public=true"
81
78
  const pairs = fullRecord.split(";").map((s) => s.trim()).filter(Boolean);
82
79
  for (const pair of pairs) {
83
80
  const eqIndex = pair.indexOf("=");
84
81
  if (eqIndex > 0) {
85
- const key = pair.slice(0, eqIndex).trim();
86
- const value = pair.slice(eqIndex + 1).trim();
87
- result[key] = value;
82
+ result[pair.slice(0, eqIndex).trim()] = pair.slice(eqIndex + 1).trim();
88
83
  }
89
84
  }
90
85
  return result;
91
86
  }
92
- // --- DNS Lookup ---
87
+ // --- DNS Lookup (returns ALL TXT records) ---
93
88
  async function lookupMcpDomain(domain) {
94
- // Normalize Unicode and detect homograph attacks
95
89
  const normalized = domain.normalize("NFC");
96
90
  const warning = detectHomograph(normalized);
97
91
  const mcpDomain = `_mcp.${normalized}`;
98
92
  try {
99
- const records = await resolveTxt(mcpDomain);
100
- if (records.length === 0) {
101
- return { record: null, ...(warning && { homograph_warning: warning }) };
93
+ const rawRecords = await resolveTxt(mcpDomain);
94
+ if (rawRecords.length === 0) {
95
+ return { records: [], ...(warning && { homograph_warning: warning }) };
102
96
  }
103
- return { record: parseMcpTxtRecord(records), ...(warning && { homograph_warning: warning }) };
97
+ const parsed = rawRecords.map((chunks) => parseSingleTxtRecord(chunks));
98
+ return { records: parsed, ...(warning && { homograph_warning: warning }) };
104
99
  }
105
100
  catch (err) {
106
101
  if (err.code === "ENODATA" || err.code === "ENOTFOUND") {
107
- return { record: null, ...(warning && { homograph_warning: warning }) };
102
+ return { records: [], ...(warning && { homograph_warning: warning }) };
108
103
  }
109
104
  throw err;
110
105
  }
111
106
  }
112
- // --- MCP Server Inspection ---
107
+ // --- MCP Server Inspection (initialize + list tools/resources/prompts) ---
113
108
  async function inspectMcpServer(url) {
114
- // Initialize handshake
115
109
  const initResponse = await fetch(url, {
116
110
  method: "POST",
117
111
  headers: { "Content-Type": "application/json" },
112
+ signal: AbortSignal.timeout(15000),
118
113
  body: JSON.stringify({
119
114
  jsonrpc: "2.0",
120
115
  id: 1,
@@ -122,7 +117,7 @@ async function inspectMcpServer(url) {
122
117
  params: {
123
118
  protocolVersion: "2024-11-05",
124
119
  capabilities: {},
125
- clientInfo: { name: "mcp-www", version: "0.1.0" },
120
+ clientInfo: { name: "mcp-www", version: "0.2.0" },
126
121
  },
127
122
  }),
128
123
  });
@@ -154,24 +149,69 @@ async function inspectMcpServer(url) {
154
149
  prompts: promptsResult?.prompts || [],
155
150
  };
156
151
  }
157
- // --- Combined Discovery + Inspection ---
158
- async function discoverMcpDomain(domain) {
159
- const { record, homograph_warning } = await lookupMcpDomain(domain);
160
- const base = { domain, ...(homograph_warning && { homograph_warning }) };
161
- if (record === null) {
162
- return { ...base, found: false, message: `No _mcp.${domain} TXT record found` };
152
+ // --- Server Card (.well-known/mcp.json) ---
153
+ async function fetchServerCard(domain) {
154
+ try {
155
+ const response = await fetch(`https://${domain}/.well-known/mcp.json`, {
156
+ signal: AbortSignal.timeout(10000),
157
+ headers: { "Accept": "application/json" },
158
+ });
159
+ if (!response.ok)
160
+ return null;
161
+ return await response.json();
163
162
  }
164
- const serverUrl = record.src || record.endpoint;
165
- if (!serverUrl) {
166
- return { ...base, found: true, record, server: null, message: "No server URL (src/endpoint) in TXT record" };
163
+ catch {
164
+ return null;
167
165
  }
168
- try {
169
- const serverInfo = await inspectMcpServer(serverUrl);
170
- return { ...base, found: true, record, server: { url: serverUrl, ...serverInfo } };
166
+ }
167
+ // --- Browse: server card first, MCP init as fallback ---
168
+ async function browseDomain(domain) {
169
+ const normalized = domain.normalize("NFC");
170
+ const warning = detectHomograph(normalized);
171
+ const { records } = await lookupMcpDomain(normalized);
172
+ const serverUrls = records
173
+ .map((r) => r.src || r.endpoint)
174
+ .filter(Boolean);
175
+ // Try server card first (cheap HTTP GET)
176
+ const serverCard = await fetchServerCard(normalized);
177
+ if (serverCard) {
178
+ return {
179
+ domain: normalized,
180
+ ...(warning && { homograph_warning: warning }),
181
+ dns_records: records,
182
+ server_card: serverCard,
183
+ server_urls: serverUrls,
184
+ };
171
185
  }
172
- catch (err) {
173
- return { ...base, found: true, record, server: { url: serverUrl, error: err.message } };
186
+ // Fallback: MCP initialize handshake on DNS-advertised servers
187
+ const serverResults = await Promise.all(serverUrls.map(async (url) => {
188
+ try {
189
+ const info = await inspectMcpServer(url);
190
+ return { url, ...info };
191
+ }
192
+ catch (err) {
193
+ return { url, error: err.message };
194
+ }
195
+ }));
196
+ return {
197
+ domain: normalized,
198
+ ...(warning && { homograph_warning: warning }),
199
+ dns_records: records,
200
+ server_card: null,
201
+ servers: serverResults,
202
+ };
203
+ }
204
+ // --- Browse by URL: server card from hostname, init as fallback ---
205
+ async function browseUrl(url) {
206
+ // Try to derive domain for server card
207
+ const hostname = new URL(url).hostname;
208
+ const serverCard = await fetchServerCard(hostname);
209
+ if (serverCard) {
210
+ return { url, server_card: serverCard };
174
211
  }
212
+ // Fallback: MCP init
213
+ const info = await inspectMcpServer(url);
214
+ return { url, ...info };
175
215
  }
176
216
  // --- Remote Server Helpers ---
177
217
  async function initRemoteServer(url) {
@@ -185,7 +225,7 @@ async function initRemoteServer(url) {
185
225
  params: {
186
226
  protocolVersion: "2024-11-05",
187
227
  capabilities: {},
188
- clientInfo: { name: "mcp-www", version: "0.1.0" },
228
+ clientInfo: { name: "mcp-www", version: "0.2.0" },
189
229
  },
190
230
  }),
191
231
  });
@@ -213,117 +253,169 @@ async function jsonRpcCall(url, id, method, params, sessionId) {
213
253
  }
214
254
  return result.result;
215
255
  }
216
- // --- Remote Tool Calling ---
217
256
  async function callRemoteTool(url, toolName, toolArgs = {}) {
218
257
  const sessionId = await initRemoteServer(url);
219
258
  return jsonRpcCall(url, 2, "tools/call", { name: toolName, arguments: toolArgs }, sessionId);
220
259
  }
221
- // --- Remote Resource Reading ---
222
260
  async function readRemoteResource(url, uri) {
223
261
  const sessionId = await initRemoteServer(url);
224
262
  return jsonRpcCall(url, 2, "resources/read", { uri }, sessionId);
225
263
  }
226
- // --- Remote Prompt Getting ---
227
264
  async function getRemotePrompt(url, promptName, promptArgs = {}) {
228
265
  const sessionId = await initRemoteServer(url);
229
266
  return jsonRpcCall(url, 2, "prompts/get", { name: promptName, arguments: promptArgs }, sessionId);
230
267
  }
231
- // --- Response Formatting ---
232
- function formatServerResult(serverData, url) {
233
- const content = [];
234
- // Surface instructions as natural language guidance the model will follow
235
- if (serverData.instructions) {
236
- content.push({
237
- type: "text",
238
- text: `[Server Instructions from ${serverData.serverInfo?.name || url}]\n` +
239
- `${serverData.instructions}\n` +
240
- `Use call_remote_tool with url "${url}" to execute any of the tools listed below.`,
241
- });
268
+ // --- Discover + Browse (lightweight: DNS + server card, init as fallback) ---
269
+ async function discoverBrowse(domain) {
270
+ const normalized = domain.normalize("NFC");
271
+ const warning = detectHomograph(normalized);
272
+ const { records } = await lookupMcpDomain(normalized);
273
+ const base = {
274
+ domain: normalized,
275
+ ...(warning && { homograph_warning: warning }),
276
+ dns_records: records,
277
+ };
278
+ if (records.length === 0) {
279
+ return { ...base, found: false, message: `No _mcp.${normalized} TXT record found` };
280
+ }
281
+ // Take the first server URL
282
+ const serverUrl = records.map((r) => r.src || r.endpoint).find(Boolean);
283
+ if (!serverUrl) {
284
+ return { ...base, found: true, server: null, message: "No server URL (src/endpoint) in TXT records" };
285
+ }
286
+ // Try server card first (cheap HTTP GET)
287
+ const serverCard = await fetchServerCard(normalized);
288
+ if (serverCard) {
289
+ return { ...base, found: true, server_url: serverUrl, server_card: serverCard };
290
+ }
291
+ // Fallback: MCP initialize handshake
292
+ try {
293
+ const serverInfo = await inspectMcpServer(serverUrl);
294
+ return { ...base, found: true, server_url: serverUrl, server: serverInfo };
295
+ }
296
+ catch (err) {
297
+ return { ...base, found: true, server_url: serverUrl, server: { error: err.message } };
242
298
  }
243
- // Add the structured data
244
- content.push({
245
- type: "text",
246
- text: JSON.stringify({ url, ...serverData }, null, 2),
247
- });
248
- return content;
299
+ }
300
+ // --- Server Registration ---
301
+ function generateRegistrationConfig(url, serverName) {
302
+ const platform = process.platform;
303
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "~";
304
+ const appData = process.env.APPDATA || "";
305
+ const configs = {
306
+ claude_desktop: {
307
+ config_path: platform === "win32"
308
+ ? `${appData}\\Claude\\claude_desktop_config.json`
309
+ : platform === "darwin"
310
+ ? `${homeDir}/Library/Application Support/Claude/claude_desktop_config.json`
311
+ : `${homeDir}/.config/Claude/claude_desktop_config.json`,
312
+ entry: { [serverName]: { url } },
313
+ merge_at: "mcpServers",
314
+ },
315
+ vscode: {
316
+ config_path: ".vscode/mcp.json",
317
+ entry: { [serverName]: { url } },
318
+ merge_at: "servers",
319
+ },
320
+ cursor: {
321
+ config_path: `${homeDir}/.cursor/mcp.json`,
322
+ entry: { [serverName]: { url } },
323
+ merge_at: "mcpServers",
324
+ },
325
+ windsurf: {
326
+ config_path: `${homeDir}/.windsurf/mcp.json`,
327
+ entry: { [serverName]: { url } },
328
+ merge_at: "mcpServers",
329
+ },
330
+ };
331
+ return {
332
+ server_name: serverName,
333
+ server_url: url,
334
+ platform,
335
+ configs,
336
+ instructions: "To register this server: read the config file for the target client, " +
337
+ "parse as JSON, add the entry object under the merge_at key, and write it back. " +
338
+ "If the file doesn't exist, create it with { [merge_at]: entry }. " +
339
+ "Do not overwrite existing server entries unless the user confirms.",
340
+ };
341
+ }
342
+ function deriveServerName(domain) {
343
+ return domain
344
+ .replace(/^mcp\./, "")
345
+ .replace(/\./g, "-")
346
+ .replace(/[^a-z0-9-]/gi, "")
347
+ .toLowerCase();
249
348
  }
250
349
  // --- Server Setup ---
251
350
  const server = new index_js_1.Server({
252
351
  name: "mcp-www",
253
- version: "0.1.0",
352
+ version: "0.2.0",
254
353
  }, {
255
354
  capabilities: {
256
355
  tools: {},
257
356
  },
258
- instructions: "When a user mentions a domain name, company, or website — or when you need to find tools and services related to a domain — use mcp-www first. " +
259
- "Start with browse_discover to look up the domain and inspect what MCP services it offers in a single step. " +
260
- "If you already have a server URL, use browse_server to see its tools, resources, and prompts. " +
261
- "Then use call_remote_tool to execute tools on discovered servers.",
357
+ instructions: "When a user mentions a domain name, company, or website — or when you need to find tools and services related to a domain — use mcp-www. " +
358
+ "Start with discover_browse for a quick one-step DNS + server card lookup. " +
359
+ "Use browse for full MCP handshake and complete server manifest. " +
360
+ "Use call_remote_tool to execute tools on discovered servers. " +
361
+ "Use install to permanently add a discovered server to the user's MCP client.",
262
362
  });
263
363
  // --- Tool Definitions ---
264
364
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
265
365
  return {
266
366
  tools: [
267
367
  {
268
- name: "browse_domain",
269
- description: "Lookup _mcp.{domain} DNS TXT records and return parsed MCP server metadata. Uses standard UDP DNS queries.",
368
+ name: "discover",
369
+ description: "DNS-only lookup. Returns all _mcp.{domain} TXT records there can be multiple, each advertising a different MCP server. Fast, cheap, no HTTP calls. Supports single domain or batch lookup across multiple domains.",
270
370
  inputSchema: {
271
371
  type: "object",
272
372
  properties: {
273
373
  domain: {
274
374
  type: "string",
275
- description: "The domain to look up (e.g., 'example.com')",
375
+ description: "A single domain to look up (e.g., 'example.com')",
276
376
  },
277
- },
278
- required: ["domain"],
279
- },
280
- },
281
- {
282
- name: "browse_server",
283
- description: "Connect to a discovered MCP server URL and retrieve its full manifest: tools, resources, and prompts. Lets you inspect what a server offers before deciding to use it.",
284
- inputSchema: {
285
- type: "object",
286
- properties: {
287
- url: {
288
- type: "string",
289
- description: "The MCP server URL (e.g., 'https://mcp.example.com')",
377
+ domains: {
378
+ type: "array",
379
+ items: { type: "string" },
380
+ description: "Multiple domains to look up in parallel",
290
381
  },
291
382
  },
292
- required: ["url"],
293
383
  },
294
384
  },
295
385
  {
296
- name: "browse_multi",
297
- description: "Batch lookup across multiple domains in a single call. Returns MCP server metadata for each domain that has _mcp TXT records.",
386
+ name: "discover_browse",
387
+ description: "DNS lookup + server card in one call. Looks up all _mcp.{domain} TXT records, then fetches .well-known/mcp.json for server metadata. Only falls back to MCP initialize if no server card is found. Lighter than browse — no MCP session unless needed.",
298
388
  inputSchema: {
299
389
  type: "object",
300
390
  properties: {
301
- domains: {
302
- type: "array",
303
- items: { type: "string" },
304
- description: "Array of domains to look up",
391
+ domain: {
392
+ type: "string",
393
+ description: "The domain to discover and browse (e.g., 'example.com')",
305
394
  },
306
395
  },
307
- required: ["domains"],
396
+ required: ["domain"],
308
397
  },
309
398
  },
310
399
  {
311
- name: "browse_discover",
312
- description: "Start here when a user mentions any domain name or website. Looks up _mcp.{domain} DNS TXT records, then connects to the advertised server URL to retrieve its full manifest (tools, resources, and prompts) all in one step.",
400
+ name: "browse",
401
+ description: "Inspect a domain or server URL. Tries .well-known/mcp.json (server card) first, only falls back to MCP initialize handshake if no server card is found. For domains: also performs DNS lookup for _mcp TXT records.",
313
402
  inputSchema: {
314
403
  type: "object",
315
404
  properties: {
316
405
  domain: {
317
406
  type: "string",
318
- description: "The domain to discover (e.g., 'example.com')",
407
+ description: "Domain to browse (e.g., 'example.com') — runs parallel server card + MCP handshake",
408
+ },
409
+ url: {
410
+ type: "string",
411
+ description: "Direct MCP server URL to inspect (e.g., 'https://mcp.example.com')",
319
412
  },
320
413
  },
321
- required: ["domain"],
322
414
  },
323
415
  },
324
416
  {
325
417
  name: "call_remote_tool",
326
- description: "Call a tool on a remote MCP server. Use browse_server first to discover available tools, then use this to execute them.",
418
+ description: "Call a tool on a remote MCP server. Use browse first to discover available tools, then use this to execute them.",
327
419
  inputSchema: {
328
420
  type: "object",
329
421
  properties: {
@@ -346,7 +438,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
346
438
  },
347
439
  {
348
440
  name: "read_remote_resource",
349
- description: "Read a resource from a remote MCP server. Use browse_server or browse_discover first to see available resources, then use this to read one by its URI.",
441
+ description: "Read a resource from a remote MCP server. Use browse first to see available resources, then use this to read one by its URI.",
350
442
  inputSchema: {
351
443
  type: "object",
352
444
  properties: {
@@ -364,7 +456,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
364
456
  },
365
457
  {
366
458
  name: "get_remote_prompt",
367
- description: "Get a prompt from a remote MCP server. Use browse_server or browse_discover first to see available prompts, then use this to retrieve one with optional arguments.",
459
+ description: "Get a prompt from a remote MCP server. Use browse first to see available prompts, then use this to retrieve one with optional arguments.",
368
460
  inputSchema: {
369
461
  type: "object",
370
462
  properties: {
@@ -385,6 +477,27 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
385
477
  required: ["url", "prompt"],
386
478
  },
387
479
  },
480
+ {
481
+ name: "install",
482
+ description: "Generate client configuration to permanently register a discovered MCP server. Returns config file paths and JSON entries for Claude Desktop, VS Code, Cursor, and Windsurf. The agent should then read the target config file, merge the entry, and write it back. Accepts a server URL directly or a domain (runs discovery first).",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ url: {
487
+ type: "string",
488
+ description: "The MCP server URL to register (e.g., 'https://mcp.example.com')",
489
+ },
490
+ domain: {
491
+ type: "string",
492
+ description: "Domain to discover first, then register the found server URL",
493
+ },
494
+ name: {
495
+ type: "string",
496
+ description: "Friendly name for the server entry (auto-derived from domain/URL if omitted)",
497
+ },
498
+ },
499
+ },
500
+ },
388
501
  ],
389
502
  };
390
503
  });
@@ -392,124 +505,121 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
392
505
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
393
506
  const { name, arguments: args } = request.params;
394
507
  switch (name) {
395
- case "browse_domain": {
396
- const domain = args.domain;
397
- try {
398
- const { record, homograph_warning } = await lookupMcpDomain(domain);
399
- const warningBlock = homograph_warning ? [formatHomographWarning(homograph_warning)] : [];
400
- if (record === null) {
401
- return {
402
- content: [
403
- ...warningBlock,
404
- {
405
- type: "text",
406
- text: JSON.stringify({ domain, found: false, message: `No _mcp.${domain} TXT record found` }, null, 2),
407
- },
408
- ],
409
- };
410
- }
411
- return {
412
- content: [
413
- ...warningBlock,
414
- {
415
- type: "text",
416
- text: JSON.stringify({ domain, found: true, record }, null, 2),
417
- },
418
- ],
419
- };
420
- }
421
- catch (err) {
508
+ case "discover": {
509
+ const { domain, domains } = args;
510
+ const domainList = domains || (domain ? [domain] : []);
511
+ if (domainList.length === 0) {
422
512
  return {
423
- content: [
424
- {
425
- type: "text",
426
- text: JSON.stringify({ domain, error: err.message }, null, 2),
427
- },
428
- ],
513
+ content: [{ type: "text", text: "Provide either 'domain' (string) or 'domains' (array)." }],
429
514
  isError: true,
430
515
  };
431
516
  }
432
- }
433
- case "browse_server": {
434
- const url = args.url;
435
- try {
436
- const result = await inspectMcpServer(url);
437
- return { content: formatServerResult(result, url) };
438
- }
439
- catch (err) {
440
- return {
441
- content: [
442
- {
443
- type: "text",
444
- text: JSON.stringify({ url, error: err.message }, null, 2),
445
- },
446
- ],
447
- isError: true,
448
- };
449
- }
450
- }
451
- case "browse_multi": {
452
- const domains = args.domains;
453
517
  const results = {};
454
518
  const warnings = [];
455
- await Promise.all(domains.map(async (domain) => {
519
+ await Promise.all(domainList.map(async (d) => {
456
520
  try {
457
- const { record, homograph_warning } = await lookupMcpDomain(domain);
458
- results[domain] = record !== null
459
- ? { found: true, record }
460
- : { found: false };
521
+ const { records, homograph_warning } = await lookupMcpDomain(d);
522
+ results[d] = records.length > 0
523
+ ? { found: true, records }
524
+ : { found: false, message: `No _mcp.${d} TXT record found` };
461
525
  if (homograph_warning)
462
- warnings.push({ domain, warning: homograph_warning });
526
+ warnings.push(formatHomographWarning(`[${d}] ${homograph_warning}`));
463
527
  }
464
528
  catch (err) {
465
- results[domain] = { error: err.message };
529
+ results[d] = { error: err.message };
466
530
  }
467
531
  }));
468
- const warningBlocks = warnings.map((w) => formatHomographWarning(`[${w.domain}] ${w.warning}`));
469
- return {
470
- content: [
471
- ...warningBlocks,
472
- {
473
- type: "text",
474
- text: JSON.stringify(results, null, 2),
475
- },
476
- ],
477
- };
532
+ const content = [
533
+ ...warnings,
534
+ { type: "text", text: JSON.stringify(domainList.length === 1 ? results[domainList[0]] : results, null, 2) },
535
+ ];
536
+ // Suggest browse for domains that have records
537
+ const domainsWithRecords = domainList.filter((d) => results[d]?.found);
538
+ if (domainsWithRecords.length > 0) {
539
+ content.push({
540
+ type: "text",
541
+ text: `Use browse to connect and inspect ${domainsWithRecords.length === 1 ? `${domainsWithRecords[0]}` : "these domains"}.`,
542
+ });
543
+ }
544
+ return { content };
478
545
  }
479
- case "browse_discover": {
546
+ case "discover_browse": {
480
547
  const domain = args.domain;
481
548
  try {
482
- const result = await discoverMcpDomain(domain);
483
- const warningBlock = result.homograph_warning ? [formatHomographWarning(result.homograph_warning)] : [];
484
- // If we got server data with instructions, surface them prominently
485
- if (result.server && !result.server.error && result.server.instructions) {
486
- const content = formatServerResult(result.server, result.server.url);
487
- // Prepend warning + discovery context
488
- content.unshift({
549
+ const result = await discoverBrowse(domain);
550
+ const content = [];
551
+ if (result.homograph_warning) {
552
+ content.push(formatHomographWarning(result.homograph_warning));
553
+ }
554
+ content.push({
555
+ type: "text",
556
+ text: JSON.stringify(result, null, 2),
557
+ });
558
+ if (result.server_url) {
559
+ content.push({
489
560
  type: "text",
490
- text: `Discovered MCP server for ${domain} via DNS lookup of _mcp.${domain}`,
561
+ text: `Use browse to get the full server manifest, or install to add this server to your MCP client.`,
491
562
  });
492
- content.unshift(...warningBlock);
493
- return { content };
494
563
  }
564
+ return { content };
565
+ }
566
+ catch (err) {
495
567
  return {
496
- content: [
497
- ...warningBlock,
498
- {
499
- type: "text",
500
- text: JSON.stringify(result, null, 2),
501
- },
502
- ],
568
+ content: [{ type: "text", text: JSON.stringify({ domain, error: err.message }, null, 2) }],
569
+ isError: true,
503
570
  };
504
571
  }
505
- catch (err) {
572
+ }
573
+ case "browse": {
574
+ const { domain, url } = args;
575
+ if (!domain && !url) {
506
576
  return {
507
- content: [
508
- {
577
+ content: [{ type: "text", text: "Provide either 'domain' or 'url'." }],
578
+ isError: true,
579
+ };
580
+ }
581
+ try {
582
+ let result;
583
+ if (url) {
584
+ result = await browseUrl(url);
585
+ }
586
+ else {
587
+ result = await browseDomain(domain);
588
+ }
589
+ const content = [];
590
+ const warningText = result.homograph_warning;
591
+ if (warningText) {
592
+ content.push(formatHomographWarning(warningText));
593
+ }
594
+ // Surface instructions from any successfully connected server
595
+ const servers = result.servers || (result.url ? [result] : []);
596
+ for (const srv of servers) {
597
+ if (srv.instructions) {
598
+ content.push({
509
599
  type: "text",
510
- text: JSON.stringify({ domain, error: err.message }, null, 2),
511
- },
512
- ],
600
+ text: `[Server Instructions from ${srv.serverInfo?.name || srv.url}]\n` +
601
+ `${srv.instructions}\n` +
602
+ `Use call_remote_tool with url "${srv.url}" to execute any of the tools listed below.`,
603
+ });
604
+ }
605
+ }
606
+ content.push({
607
+ type: "text",
608
+ text: JSON.stringify(result, null, 2),
609
+ });
610
+ // Suggest register for servers that connected successfully
611
+ const connectedUrls = servers.filter((s) => !s.error && s.serverInfo).map((s) => s.url);
612
+ if (connectedUrls.length > 0) {
613
+ content.push({
614
+ type: "text",
615
+ text: `To permanently add ${connectedUrls.length === 1 ? "this server" : "these servers"} to the user's MCP client, use install.`,
616
+ });
617
+ }
618
+ return { content };
619
+ }
620
+ catch (err) {
621
+ return {
622
+ content: [{ type: "text", text: JSON.stringify({ domain, url, error: err.message }, null, 2) }],
513
623
  isError: true,
514
624
  };
515
625
  }
@@ -526,12 +636,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
526
636
  }
527
637
  catch (err) {
528
638
  return {
529
- content: [
530
- {
531
- type: "text",
532
- text: JSON.stringify({ url, tool, error: err.message }, null, 2),
533
- },
534
- ],
639
+ content: [{ type: "text", text: JSON.stringify({ url, tool, error: err.message }, null, 2) }],
535
640
  isError: true,
536
641
  };
537
642
  }
@@ -551,12 +656,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
551
656
  }
552
657
  catch (err) {
553
658
  return {
554
- content: [
555
- {
556
- type: "text",
557
- text: JSON.stringify({ url, uri, error: err.message }, null, 2),
558
- },
559
- ],
659
+ content: [{ type: "text", text: JSON.stringify({ url, uri, error: err.message }, null, 2) }],
560
660
  isError: true,
561
661
  };
562
662
  }
@@ -566,22 +666,68 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
566
666
  try {
567
667
  const result = await getRemotePrompt(url, prompt, promptArgs || {});
568
668
  return {
569
- content: [
570
- { type: "text", text: JSON.stringify(result, null, 2) },
571
- ],
669
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
572
670
  };
573
671
  }
574
672
  catch (err) {
575
673
  return {
576
- content: [
577
- {
578
- type: "text",
579
- text: JSON.stringify({ url, prompt, error: err.message }, null, 2),
580
- },
581
- ],
674
+ content: [{ type: "text", text: JSON.stringify({ url, prompt, error: err.message }, null, 2) }],
675
+ isError: true,
676
+ };
677
+ }
678
+ }
679
+ case "install": {
680
+ const { url, domain, name: serverName } = args;
681
+ if (!url && !domain) {
682
+ return {
683
+ content: [{ type: "text", text: "Provide either 'url' (server URL) or 'domain' (to discover first)." }],
582
684
  isError: true,
583
685
  };
584
686
  }
687
+ let serverUrl = url;
688
+ let discoveredDomain = domain;
689
+ // If domain provided, discover the server URL first
690
+ if (!serverUrl && domain) {
691
+ try {
692
+ const { records } = await lookupMcpDomain(domain);
693
+ const firstUrl = records.map((r) => r.src || r.endpoint).find(Boolean);
694
+ if (firstUrl) {
695
+ serverUrl = firstUrl;
696
+ }
697
+ }
698
+ catch (err) {
699
+ return {
700
+ content: [{ type: "text", text: JSON.stringify({ domain, error: `Discovery failed: ${err.message}` }, null, 2) }],
701
+ isError: true,
702
+ };
703
+ }
704
+ if (!serverUrl) {
705
+ return {
706
+ content: [{ type: "text", text: JSON.stringify({ domain, error: "No MCP server URL found for this domain. Cannot register." }, null, 2) }],
707
+ isError: true,
708
+ };
709
+ }
710
+ }
711
+ const derivedName = serverName || deriveServerName(discoveredDomain || new URL(serverUrl).hostname);
712
+ const config = generateRegistrationConfig(serverUrl, derivedName);
713
+ const content = [];
714
+ if (discoveredDomain) {
715
+ content.push({
716
+ type: "text",
717
+ text: `Discovered server at ${serverUrl} via DNS lookup of _mcp.${discoveredDomain}`,
718
+ });
719
+ }
720
+ content.push({
721
+ type: "text",
722
+ text: JSON.stringify(config, null, 2),
723
+ });
724
+ content.push({
725
+ type: "text",
726
+ text: `To complete registration, read the config file for the target client, ` +
727
+ `merge the entry under the "${config.configs.claude_desktop.merge_at}" key, and write it back. ` +
728
+ `If the file doesn't exist, create it.`,
729
+ });
730
+ return { content };
585
731
  }
586
732
  default:
587
733
  throw new Error(`Unknown tool: ${name}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-www",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight MCP server for DNS-based agent service discovery over UDP. No registry needed.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",