mcp-www 0.1.3 → 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 +202 -193
  2. package/dist/index.js +410 -192
  3. package/package.json +45 -45
package/README.md CHANGED
@@ -1,193 +1,202 @@
1
- # mcp-www
2
-
3
- [![npm version](https://img.shields.io/npm/v/mcp-www)](https://www.npmjs.com/package/mcp-www)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
6
-
7
- **DNS-based MCP service discovery over UDP.**
8
-
9
- ## Problem
10
-
11
- Agents need to discover MCP servers, but current approaches lean on centralized registries or hardcoded configurations. This creates single points of failure, adds deployment overhead, and forces agents into walled gardens. There should be a way to discover MCP services using infrastructure that already exists everywhere: DNS.
12
-
13
- ## How It Works
14
-
15
- **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.
16
-
17
- 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
-
19
- ```
20
- Agent → mcp-www (MCP server) → UDP DNS query for _mcp.example.com TXT
21
- "v=mcp1; src=https://mcp.example.com; ..."
22
- Structured JSON response
23
- ```
24
-
25
- No HTTP registry in the loop. The DNS infrastructure **is** the registry.
26
-
27
- ## Install
28
-
29
- ```bash
30
- npm install -g mcp-www
31
- ```
32
-
33
- Or use directly with `npx`:
34
-
35
- ```bash
36
- npx mcp-www
37
- ```
38
-
39
- ### Claude Code / MCP Client Config
40
-
41
- Add to your MCP client config (e.g., `.mcp.json`):
42
-
43
- ```json
44
- {
45
- "mcpServers": {
46
- "mcp-www": {
47
- "type": "stdio",
48
- "command": "npx",
49
- "args": ["mcp-www"]
50
- }
51
- }
52
- }
53
- ```
54
-
55
- ## Key Design Points
56
-
57
- - **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.
58
- - **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.
59
- - **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.
60
- - **Supports the `_mcp` TXT record convention** — records follow a semicolon-delimited format:
61
- ```
62
- v=mcp1; src=https://mcp.example.com; auth=oauth2
63
- ```
64
- - **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.
65
-
66
- ## Tools Exposed
67
-
68
- ### `browse_domain`
69
-
70
- Lookup `_mcp.{domain}` TXT records and return a parsed list of discovered MCP servers.
71
-
72
- ```json
73
- {
74
- "tool": "browse_domain",
75
- "arguments": {
76
- "domain": "example.com"
77
- }
78
- }
79
- ```
80
-
81
- Returns structured server metadata: server URL, protocol version, auth requirements, and any additional fields published in the TXT record.
82
-
83
- ### `browse_discover`
84
-
85
- 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.
86
-
87
- ```json
88
- {
89
- "tool": "browse_discover",
90
- "arguments": {
91
- "domain": "example.com"
92
- }
93
- }
94
- ```
95
-
96
- ### `browse_server`
97
-
98
- 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.
99
-
100
- ```json
101
- {
102
- "tool": "browse_server",
103
- "arguments": {
104
- "url": "https://mcp.example.com"
105
- }
106
- }
107
- ```
108
-
109
- ### `browse_multi`
110
-
111
- Batch lookup across multiple domains in a single call. Useful for scanning a list of known domains or performing broad discovery.
112
-
113
- ```json
114
- {
115
- "tool": "browse_multi",
116
- "arguments": {
117
- "domains": ["example.com", "acme.org", "internal.corp"]
118
- }
119
- }
120
- ```
121
-
122
- ### `call_remote_tool`
123
-
124
- 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.
125
-
126
- ```json
127
- {
128
- "tool": "call_remote_tool",
129
- "arguments": {
130
- "url": "https://mcp.example.com",
131
- "tool": "list_articles",
132
- "arguments": { "limit": 5 }
133
- }
134
- }
135
- ```
136
-
137
- ### `read_remote_resource`
138
-
139
- 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.
140
-
141
- ```json
142
- {
143
- "tool": "read_remote_resource",
144
- "arguments": {
145
- "url": "https://mcp.example.com",
146
- "uri": "korm://bio"
147
- }
148
- }
149
- ```
150
-
151
- ### `get_remote_prompt`
152
-
153
- 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.
154
-
155
- ```json
156
- {
157
- "tool": "get_remote_prompt",
158
- "arguments": {
159
- "url": "https://mcp.example.com",
160
- "prompt": "recommend-post",
161
- "arguments": { "topic": "AI vision" }
162
- }
163
- }
164
- ```
165
-
166
- ## Try It
167
-
168
- **[korm.co](https://korm.co)** publishes a live `_mcp` TXT record. You can discover and interact with it end-to-end:
169
-
170
- ```
171
- browse_discover("korm.co") → discovers server, returns tools + resources + prompts
172
- browse_server("https://mcp.korm.co") → inspects tools, resources, and prompts
173
- call_remote_tool("https://mcp.korm.co", "list_articles") → returns blog articles
174
- read_remote_resource("https://mcp.korm.co", "korm://bio") reads author bio
175
- get_remote_prompt("https://mcp.korm.co", "recommend-post", { "topic": "AI" }) → gets prompt
176
- ```
177
-
178
- ## Status
179
-
180
- **Working.** The server implements DNS-based discovery, server inspection, remote tool calling, resource reading, and prompt retrieval over the Streamable HTTP transport.
181
-
182
- Feedback, criticism, and alternative approaches are welcome open an issue or start a discussion.
183
-
184
- ## Related
185
-
186
- - [Model Context Protocol Specification](https://modelcontextprotocol.io/specification)
187
- - [DNS TXT records for organisation-scoped registry discovery](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/2334) — Discussion #2334
188
- - [DNS-native MCP discovery: a zero-infrastructure alternative](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/2368) — Discussion #2368
189
- - [SEP-2127: MCP Server Cards HTTP Server Discovery via .well-known](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127)
190
-
191
- ## License
192
-
193
- MIT
1
+ # mcp-www
2
+
3
+ [![npm version](https://img.shields.io/npm/v/mcp-www)](https://www.npmjs.com/package/mcp-www)
4
+ [![npm downloads](https://img.shields.io/npm/dm/mcp-www)](https://www.npmjs.com/package/mcp-www)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+
8
+ **DNS-based MCP service discovery and installation.**
9
+
10
+ ## Problem
11
+
12
+ Agents need to discover MCP servers, but current approaches lean on centralized registries or hardcoded configurations. This creates single points of failure, adds deployment overhead, and forces agents into walled gardens. There should be a way to discover MCP services using infrastructure that already exists everywhere: DNS.
13
+
14
+ ## How It Works
15
+
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
+
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
+
20
+ ```
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
+ ```
25
+
26
+ No HTTP registry in the loop. The DNS infrastructure **is** the registry.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install -g mcp-www
32
+ ```
33
+
34
+ Or use directly with `npx`:
35
+
36
+ ```bash
37
+ npx mcp-www
38
+ ```
39
+
40
+ ### Claude Code / MCP Client Config
41
+
42
+ Add to your MCP client config (e.g., `.mcp.json`):
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "mcp-www": {
48
+ "type": "stdio",
49
+ "command": "npx",
50
+ "args": ["mcp-www"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Try It
57
+
58
+ **[korm.co](https://korm.co)** publishes a live `_mcp` TXT record. You can discover and interact with it end-to-end:
59
+
60
+ ```
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
68
+ ```
69
+
70
+ ## Key Design Points
71
+
72
+ - **Uses UDP DNS (port 53) for lookups** — the lightest possible network primitive. A single UDP packet out, a single packet back.
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.
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:**
77
+ ```
78
+ v=mcp1; src=https://mcp.example.com; auth=oauth2
79
+ ```
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`
82
+
83
+ ## Tools
84
+
85
+ ### `discover`
86
+
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.
88
+
89
+ ```json
90
+ { "tool": "discover", "arguments": { "domain": "example.com" } }
91
+ { "tool": "discover", "arguments": { "domains": ["example.com", "acme.org"] } }
92
+ ```
93
+
94
+ ### `discover_browse`
95
+
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.
97
+
98
+ ```json
99
+ { "tool": "discover_browse", "arguments": { "domain": "example.com" } }
100
+ ```
101
+
102
+ ### `browse`
103
+
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.
105
+
106
+ ```json
107
+ { "tool": "browse", "arguments": { "domain": "example.com" } }
108
+ { "tool": "browse", "arguments": { "url": "https://mcp.example.com" } }
109
+ ```
110
+
111
+ ### `call_remote_tool`
112
+
113
+ Call a tool on a remote MCP server. Use `browse` first to discover available tools, then use this to execute them.
114
+
115
+ ```json
116
+ {
117
+ "tool": "call_remote_tool",
118
+ "arguments": {
119
+ "url": "https://mcp.example.com",
120
+ "tool": "list_articles",
121
+ "arguments": { "limit": 5 }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### `read_remote_resource`
127
+
128
+ Read a resource from a remote MCP server by its URI.
129
+
130
+ ```json
131
+ {
132
+ "tool": "read_remote_resource",
133
+ "arguments": {
134
+ "url": "https://mcp.example.com",
135
+ "uri": "korm://bio"
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### `get_remote_prompt`
141
+
142
+ Get a prompt from a remote MCP server with optional arguments.
143
+
144
+ ```json
145
+ {
146
+ "tool": "get_remote_prompt",
147
+ "arguments": {
148
+ "url": "https://mcp.example.com",
149
+ "prompt": "recommend-post",
150
+ "arguments": { "topic": "AI vision" }
151
+ }
152
+ }
153
+ ```
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
+ ```
163
+
164
+ ## Status
165
+
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.
167
+
168
+ Feedback, criticism, and alternative approaches are welcome open an issue or start a discussion.
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
+
193
+ ## Related
194
+
195
+ - [Model Context Protocol Specification](https://modelcontextprotocol.io/specification)
196
+ - [DNS TXT records for organisation-scoped registry discovery](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/2334) — Discussion #2334
197
+ - [DNS-native MCP discovery: a zero-infrastructure alternative](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/2368) — Discussion #2368
198
+ - [SEP-2127: MCP Server Cards — HTTP Server Discovery via .well-known](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127)
199
+
200
+ ## License
201
+
202
+ MIT
package/dist/index.js CHANGED
@@ -15,48 +15,101 @@ 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);
24
+ // --- IDN Homograph Detection ---
25
+ function detectHomograph(domain) {
26
+ const labels = domain.split(".");
27
+ // Check for punycode-encoded labels (ACE prefix)
28
+ const punycodeLabels = labels.filter((l) => l.startsWith("xn--"));
29
+ if (punycodeLabels.length > 0) {
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.`;
31
+ }
32
+ // Check for mixed Unicode scripts within a single label
33
+ for (const label of labels) {
34
+ let hasLatin = false;
35
+ let hasNonLatin = false;
36
+ let detectedScripts = [];
37
+ for (const char of label) {
38
+ const code = char.codePointAt(0);
39
+ if (code >= 0x41 && code <= 0x7a) {
40
+ hasLatin = true;
41
+ }
42
+ else if (code >= 0x0400 && code <= 0x04ff) {
43
+ hasNonLatin = true;
44
+ if (!detectedScripts.includes("Cyrillic"))
45
+ detectedScripts.push("Cyrillic");
46
+ }
47
+ else if (code >= 0x0370 && code <= 0x03ff) {
48
+ hasNonLatin = true;
49
+ if (!detectedScripts.includes("Greek"))
50
+ detectedScripts.push("Greek");
51
+ }
52
+ }
53
+ if (hasLatin && hasNonLatin) {
54
+ return `Domain label "${label}" mixes Latin with ${detectedScripts.join("/")} characters. This is a strong indicator of an IDN homograph attack.`;
55
+ }
56
+ if (hasNonLatin && !hasLatin && detectedScripts.length > 0) {
57
+ return `Domain label "${label}" uses ${detectedScripts.join("/")} script characters that may visually mimic Latin characters. Verify this is the intended domain.`;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ function formatHomographWarning(warning) {
63
+ return {
64
+ type: "text",
65
+ text: [
66
+ "////// SECURITY WARNING //////",
67
+ "IDN HOMOGRAPH ATTACK DETECTED",
68
+ warning,
69
+ "DO NOT proceed without verifying this is the intended domain. Ask the user to confirm.",
70
+ "//////////////////////////////",
71
+ ].join("\n"),
72
+ };
73
+ }
19
74
  // --- TXT Record Parser ---
20
- function parseMcpTxtRecord(txtRecords) {
21
- // TXT records come as arrays of strings (chunked), join them
22
- const fullRecord = txtRecords.map((chunks) => chunks.join("")).join("");
75
+ function parseSingleTxtRecord(chunks) {
76
+ const fullRecord = chunks.join("");
23
77
  const result = {};
24
- // Parse semicolon-delimited key=value pairs
25
- // e.g. "v=mcp1; endpoint=https://...; public=true"
26
78
  const pairs = fullRecord.split(";").map((s) => s.trim()).filter(Boolean);
27
79
  for (const pair of pairs) {
28
80
  const eqIndex = pair.indexOf("=");
29
81
  if (eqIndex > 0) {
30
- const key = pair.slice(0, eqIndex).trim();
31
- const value = pair.slice(eqIndex + 1).trim();
32
- result[key] = value;
82
+ result[pair.slice(0, eqIndex).trim()] = pair.slice(eqIndex + 1).trim();
33
83
  }
34
84
  }
35
85
  return result;
36
86
  }
37
- // --- DNS Lookup ---
87
+ // --- DNS Lookup (returns ALL TXT records) ---
38
88
  async function lookupMcpDomain(domain) {
39
- const mcpDomain = `_mcp.${domain}`;
89
+ const normalized = domain.normalize("NFC");
90
+ const warning = detectHomograph(normalized);
91
+ const mcpDomain = `_mcp.${normalized}`;
40
92
  try {
41
- const records = await resolveTxt(mcpDomain);
42
- if (records.length === 0) {
43
- return null;
93
+ const rawRecords = await resolveTxt(mcpDomain);
94
+ if (rawRecords.length === 0) {
95
+ return { records: [], ...(warning && { homograph_warning: warning }) };
44
96
  }
45
- return parseMcpTxtRecord(records);
97
+ const parsed = rawRecords.map((chunks) => parseSingleTxtRecord(chunks));
98
+ return { records: parsed, ...(warning && { homograph_warning: warning }) };
46
99
  }
47
100
  catch (err) {
48
101
  if (err.code === "ENODATA" || err.code === "ENOTFOUND") {
49
- return null; // No TXT record found
102
+ return { records: [], ...(warning && { homograph_warning: warning }) };
50
103
  }
51
104
  throw err;
52
105
  }
53
106
  }
54
- // --- MCP Server Inspection ---
107
+ // --- MCP Server Inspection (initialize + list tools/resources/prompts) ---
55
108
  async function inspectMcpServer(url) {
56
- // Initialize handshake
57
109
  const initResponse = await fetch(url, {
58
110
  method: "POST",
59
111
  headers: { "Content-Type": "application/json" },
112
+ signal: AbortSignal.timeout(15000),
60
113
  body: JSON.stringify({
61
114
  jsonrpc: "2.0",
62
115
  id: 1,
@@ -64,7 +117,7 @@ async function inspectMcpServer(url) {
64
117
  params: {
65
118
  protocolVersion: "2024-11-05",
66
119
  capabilities: {},
67
- clientInfo: { name: "mcp-www", version: "0.1.0" },
120
+ clientInfo: { name: "mcp-www", version: "0.2.0" },
68
121
  },
69
122
  }),
70
123
  });
@@ -96,23 +149,69 @@ async function inspectMcpServer(url) {
96
149
  prompts: promptsResult?.prompts || [],
97
150
  };
98
151
  }
99
- // --- Combined Discovery + Inspection ---
100
- async function discoverMcpDomain(domain) {
101
- const record = await lookupMcpDomain(domain);
102
- if (record === null) {
103
- return { domain, 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();
104
162
  }
105
- const serverUrl = record.src || record.endpoint;
106
- if (!serverUrl) {
107
- return { domain, found: true, record, server: null, message: "No server URL (src/endpoint) in TXT record" };
163
+ catch {
164
+ return null;
108
165
  }
109
- try {
110
- const serverInfo = await inspectMcpServer(serverUrl);
111
- return { domain, 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
+ };
112
185
  }
113
- catch (err) {
114
- return { domain, 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 };
115
211
  }
212
+ // Fallback: MCP init
213
+ const info = await inspectMcpServer(url);
214
+ return { url, ...info };
116
215
  }
117
216
  // --- Remote Server Helpers ---
118
217
  async function initRemoteServer(url) {
@@ -126,7 +225,7 @@ async function initRemoteServer(url) {
126
225
  params: {
127
226
  protocolVersion: "2024-11-05",
128
227
  capabilities: {},
129
- clientInfo: { name: "mcp-www", version: "0.1.0" },
228
+ clientInfo: { name: "mcp-www", version: "0.2.0" },
130
229
  },
131
230
  }),
132
231
  });
@@ -154,117 +253,169 @@ async function jsonRpcCall(url, id, method, params, sessionId) {
154
253
  }
155
254
  return result.result;
156
255
  }
157
- // --- Remote Tool Calling ---
158
256
  async function callRemoteTool(url, toolName, toolArgs = {}) {
159
257
  const sessionId = await initRemoteServer(url);
160
258
  return jsonRpcCall(url, 2, "tools/call", { name: toolName, arguments: toolArgs }, sessionId);
161
259
  }
162
- // --- Remote Resource Reading ---
163
260
  async function readRemoteResource(url, uri) {
164
261
  const sessionId = await initRemoteServer(url);
165
262
  return jsonRpcCall(url, 2, "resources/read", { uri }, sessionId);
166
263
  }
167
- // --- Remote Prompt Getting ---
168
264
  async function getRemotePrompt(url, promptName, promptArgs = {}) {
169
265
  const sessionId = await initRemoteServer(url);
170
266
  return jsonRpcCall(url, 2, "prompts/get", { name: promptName, arguments: promptArgs }, sessionId);
171
267
  }
172
- // --- Response Formatting ---
173
- function formatServerResult(serverData, url) {
174
- const content = [];
175
- // Surface instructions as natural language guidance the model will follow
176
- if (serverData.instructions) {
177
- content.push({
178
- type: "text",
179
- text: `[Server Instructions from ${serverData.serverInfo?.name || url}]\n` +
180
- `${serverData.instructions}\n` +
181
- `Use call_remote_tool with url "${url}" to execute any of the tools listed below.`,
182
- });
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` };
183
280
  }
184
- // Add the structured data
185
- content.push({
186
- type: "text",
187
- text: JSON.stringify({ url, ...serverData }, null, 2),
188
- });
189
- return content;
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 } };
298
+ }
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();
190
348
  }
191
349
  // --- Server Setup ---
192
350
  const server = new index_js_1.Server({
193
351
  name: "mcp-www",
194
- version: "0.1.0",
352
+ version: "0.2.0",
195
353
  }, {
196
354
  capabilities: {
197
355
  tools: {},
198
356
  },
199
- 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. " +
200
- "Start with browse_discover to look up the domain and inspect what MCP services it offers in a single step. " +
201
- "If you already have a server URL, use browse_server to see its tools, resources, and prompts. " +
202
- "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.",
203
362
  });
204
363
  // --- Tool Definitions ---
205
364
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
206
365
  return {
207
366
  tools: [
208
367
  {
209
- name: "browse_domain",
210
- 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.",
211
370
  inputSchema: {
212
371
  type: "object",
213
372
  properties: {
214
373
  domain: {
215
374
  type: "string",
216
- description: "The domain to look up (e.g., 'example.com')",
375
+ description: "A single domain to look up (e.g., 'example.com')",
217
376
  },
218
- },
219
- required: ["domain"],
220
- },
221
- },
222
- {
223
- name: "browse_server",
224
- 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.",
225
- inputSchema: {
226
- type: "object",
227
- properties: {
228
- url: {
229
- type: "string",
230
- 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",
231
381
  },
232
382
  },
233
- required: ["url"],
234
383
  },
235
384
  },
236
385
  {
237
- name: "browse_multi",
238
- 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.",
239
388
  inputSchema: {
240
389
  type: "object",
241
390
  properties: {
242
- domains: {
243
- type: "array",
244
- items: { type: "string" },
245
- description: "Array of domains to look up",
391
+ domain: {
392
+ type: "string",
393
+ description: "The domain to discover and browse (e.g., 'example.com')",
246
394
  },
247
395
  },
248
- required: ["domains"],
396
+ required: ["domain"],
249
397
  },
250
398
  },
251
399
  {
252
- name: "browse_discover",
253
- 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.",
254
402
  inputSchema: {
255
403
  type: "object",
256
404
  properties: {
257
405
  domain: {
258
406
  type: "string",
259
- 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')",
260
412
  },
261
413
  },
262
- required: ["domain"],
263
414
  },
264
415
  },
265
416
  {
266
417
  name: "call_remote_tool",
267
- 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.",
268
419
  inputSchema: {
269
420
  type: "object",
270
421
  properties: {
@@ -287,7 +438,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
287
438
  },
288
439
  {
289
440
  name: "read_remote_resource",
290
- 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.",
291
442
  inputSchema: {
292
443
  type: "object",
293
444
  properties: {
@@ -305,7 +456,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
305
456
  },
306
457
  {
307
458
  name: "get_remote_prompt",
308
- 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.",
309
460
  inputSchema: {
310
461
  type: "object",
311
462
  properties: {
@@ -326,6 +477,27 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
326
477
  required: ["url", "prompt"],
327
478
  },
328
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
+ },
329
501
  ],
330
502
  };
331
503
  });
@@ -333,111 +505,121 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
333
505
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
334
506
  const { name, arguments: args } = request.params;
335
507
  switch (name) {
336
- case "browse_domain": {
508
+ case "discover": {
509
+ const { domain, domains } = args;
510
+ const domainList = domains || (domain ? [domain] : []);
511
+ if (domainList.length === 0) {
512
+ return {
513
+ content: [{ type: "text", text: "Provide either 'domain' (string) or 'domains' (array)." }],
514
+ isError: true,
515
+ };
516
+ }
517
+ const results = {};
518
+ const warnings = [];
519
+ await Promise.all(domainList.map(async (d) => {
520
+ try {
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` };
525
+ if (homograph_warning)
526
+ warnings.push(formatHomographWarning(`[${d}] ${homograph_warning}`));
527
+ }
528
+ catch (err) {
529
+ results[d] = { error: err.message };
530
+ }
531
+ }));
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 };
545
+ }
546
+ case "discover_browse": {
337
547
  const domain = args.domain;
338
548
  try {
339
- const result = await lookupMcpDomain(domain);
340
- if (result === null) {
341
- return {
342
- content: [
343
- {
344
- type: "text",
345
- text: JSON.stringify({ domain, found: false, message: `No _mcp.${domain} TXT record found` }, null, 2),
346
- },
347
- ],
348
- };
549
+ const result = await discoverBrowse(domain);
550
+ const content = [];
551
+ if (result.homograph_warning) {
552
+ content.push(formatHomographWarning(result.homograph_warning));
349
553
  }
350
- return {
351
- content: [
352
- {
353
- type: "text",
354
- text: JSON.stringify({ domain, found: true, record: result }, null, 2),
355
- },
356
- ],
357
- };
554
+ content.push({
555
+ type: "text",
556
+ text: JSON.stringify(result, null, 2),
557
+ });
558
+ if (result.server_url) {
559
+ content.push({
560
+ type: "text",
561
+ text: `Use browse to get the full server manifest, or install to add this server to your MCP client.`,
562
+ });
563
+ }
564
+ return { content };
358
565
  }
359
566
  catch (err) {
360
567
  return {
361
- content: [
362
- {
363
- type: "text",
364
- text: JSON.stringify({ domain, error: err.message }, null, 2),
365
- },
366
- ],
568
+ content: [{ type: "text", text: JSON.stringify({ domain, error: err.message }, null, 2) }],
367
569
  isError: true,
368
570
  };
369
571
  }
370
572
  }
371
- case "browse_server": {
372
- const url = args.url;
373
- try {
374
- const result = await inspectMcpServer(url);
375
- return { content: formatServerResult(result, url) };
376
- }
377
- catch (err) {
573
+ case "browse": {
574
+ const { domain, url } = args;
575
+ if (!domain && !url) {
378
576
  return {
379
- content: [
380
- {
381
- type: "text",
382
- text: JSON.stringify({ url, error: err.message }, null, 2),
383
- },
384
- ],
577
+ content: [{ type: "text", text: "Provide either 'domain' or 'url'." }],
385
578
  isError: true,
386
579
  };
387
580
  }
388
- }
389
- case "browse_multi": {
390
- const domains = args.domains;
391
- const results = {};
392
- await Promise.all(domains.map(async (domain) => {
393
- try {
394
- const record = await lookupMcpDomain(domain);
395
- results[domain] = record !== null ? { found: true, record } : { found: false };
581
+ try {
582
+ let result;
583
+ if (url) {
584
+ result = await browseUrl(url);
396
585
  }
397
- catch (err) {
398
- results[domain] = { error: err.message };
586
+ else {
587
+ result = await browseDomain(domain);
399
588
  }
400
- }));
401
- return {
402
- content: [
403
- {
404
- type: "text",
405
- text: JSON.stringify(results, null, 2),
406
- },
407
- ],
408
- };
409
- }
410
- case "browse_discover": {
411
- const domain = args.domain;
412
- try {
413
- const result = await discoverMcpDomain(domain);
414
- // If we got server data with instructions, surface them prominently
415
- if (result.server && !result.server.error && result.server.instructions) {
416
- const content = formatServerResult(result.server, result.server.url);
417
- // Prepend the discovery context
418
- content.unshift({
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({
599
+ type: "text",
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({
419
614
  type: "text",
420
- text: `Discovered MCP server for ${domain} via DNS lookup of _mcp.${domain}`,
615
+ text: `To permanently add ${connectedUrls.length === 1 ? "this server" : "these servers"} to the user's MCP client, use install.`,
421
616
  });
422
- return { content };
423
617
  }
424
- return {
425
- content: [
426
- {
427
- type: "text",
428
- text: JSON.stringify(result, null, 2),
429
- },
430
- ],
431
- };
618
+ return { content };
432
619
  }
433
620
  catch (err) {
434
621
  return {
435
- content: [
436
- {
437
- type: "text",
438
- text: JSON.stringify({ domain, error: err.message }, null, 2),
439
- },
440
- ],
622
+ content: [{ type: "text", text: JSON.stringify({ domain, url, error: err.message }, null, 2) }],
441
623
  isError: true,
442
624
  };
443
625
  }
@@ -454,12 +636,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
454
636
  }
455
637
  catch (err) {
456
638
  return {
457
- content: [
458
- {
459
- type: "text",
460
- text: JSON.stringify({ url, tool, error: err.message }, null, 2),
461
- },
462
- ],
639
+ content: [{ type: "text", text: JSON.stringify({ url, tool, error: err.message }, null, 2) }],
463
640
  isError: true,
464
641
  };
465
642
  }
@@ -479,12 +656,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
479
656
  }
480
657
  catch (err) {
481
658
  return {
482
- content: [
483
- {
484
- type: "text",
485
- text: JSON.stringify({ url, uri, error: err.message }, null, 2),
486
- },
487
- ],
659
+ content: [{ type: "text", text: JSON.stringify({ url, uri, error: err.message }, null, 2) }],
488
660
  isError: true,
489
661
  };
490
662
  }
@@ -494,22 +666,68 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
494
666
  try {
495
667
  const result = await getRemotePrompt(url, prompt, promptArgs || {});
496
668
  return {
497
- content: [
498
- { type: "text", text: JSON.stringify(result, null, 2) },
499
- ],
669
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
500
670
  };
501
671
  }
502
672
  catch (err) {
503
673
  return {
504
- content: [
505
- {
506
- type: "text",
507
- text: JSON.stringify({ url, prompt, error: err.message }, null, 2),
508
- },
509
- ],
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)." }],
510
684
  isError: true,
511
685
  };
512
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 };
513
731
  }
514
732
  default:
515
733
  throw new Error(`Unknown tool: ${name}`);
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
- {
2
- "name": "mcp-www",
3
- "version": "0.1.3",
4
- "description": "Lightweight MCP server for DNS-based agent service discovery over UDP. No registry needed.",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "bin": {
8
- "mcp-www": "dist/index.js"
9
- },
10
- "files": [
11
- "dist",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "build": "tsc",
16
- "prepublishOnly": "npm run build",
17
- "start": "node dist/index.js",
18
- "dev": "ts-node src/index.ts"
19
- },
20
- "keywords": [
21
- "mcp",
22
- "dns",
23
- "service-discovery",
24
- "udp",
25
- "model-context-protocol",
26
- "agent"
27
- ],
28
- "author": "kormco",
29
- "license": "MIT",
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/kormco/mcp-www.git"
33
- },
34
- "engines": {
35
- "node": ">=18"
36
- },
37
- "dependencies": {
38
- "@modelcontextprotocol/sdk": "^1.27.0"
39
- },
40
- "devDependencies": {
41
- "typescript": "^5.4.0",
42
- "ts-node": "^10.9.0",
43
- "@types/node": "^20.0.0"
44
- }
45
- }
1
+ {
2
+ "name": "mcp-www",
3
+ "version": "0.2.0",
4
+ "description": "Lightweight MCP server for DNS-based agent service discovery over UDP. No registry needed.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "mcp-www": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build",
17
+ "start": "node dist/index.js",
18
+ "dev": "ts-node src/index.ts"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "dns",
23
+ "service-discovery",
24
+ "udp",
25
+ "model-context-protocol",
26
+ "agent"
27
+ ],
28
+ "author": "kormco",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/kormco/mcp-www.git"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.27.0"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.4.0",
42
+ "ts-node": "^10.9.0",
43
+ "@types/node": "^20.0.0"
44
+ }
45
+ }