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.
- package/README.md +65 -64
- package/dist/index.js +356 -210
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
**DNS-based MCP service discovery
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
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.
|
|
79
|
-
- **
|
|
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
|
|
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
|
|
83
|
+
## Tools
|
|
86
84
|
|
|
87
|
-
### `
|
|
85
|
+
### `discover`
|
|
88
86
|
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
### `browse_discover`
|
|
94
|
+
### `discover_browse`
|
|
103
95
|
|
|
104
|
-
|
|
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
|
-
### `
|
|
102
|
+
### `browse`
|
|
129
103
|
|
|
130
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
100
|
-
if (
|
|
101
|
-
return {
|
|
93
|
+
const rawRecords = await resolveTxt(mcpDomain);
|
|
94
|
+
if (rawRecords.length === 0) {
|
|
95
|
+
return { records: [], ...(warning && { homograph_warning: warning }) };
|
|
102
96
|
}
|
|
103
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
// ---
|
|
158
|
-
async function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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.
|
|
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
|
-
// ---
|
|
232
|
-
function
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
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
|
|
259
|
-
"Start with
|
|
260
|
-
"
|
|
261
|
-
"
|
|
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: "
|
|
269
|
-
description: "
|
|
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: "
|
|
375
|
+
description: "A single domain to look up (e.g., 'example.com')",
|
|
276
376
|
},
|
|
277
|
-
|
|
278
|
-
|
|
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: "
|
|
297
|
-
description: "
|
|
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
|
-
|
|
302
|
-
type: "
|
|
303
|
-
|
|
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: ["
|
|
396
|
+
required: ["domain"],
|
|
308
397
|
},
|
|
309
398
|
},
|
|
310
399
|
{
|
|
311
|
-
name: "
|
|
312
|
-
description: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
396
|
-
const domain = args
|
|
397
|
-
|
|
398
|
-
|
|
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(
|
|
519
|
+
await Promise.all(domainList.map(async (d) => {
|
|
456
520
|
try {
|
|
457
|
-
const {
|
|
458
|
-
results[
|
|
459
|
-
? { found: true,
|
|
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({
|
|
526
|
+
warnings.push(formatHomographWarning(`[${d}] ${homograph_warning}`));
|
|
463
527
|
}
|
|
464
528
|
catch (err) {
|
|
465
|
-
results[
|
|
529
|
+
results[d] = { error: err.message };
|
|
466
530
|
}
|
|
467
531
|
}));
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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 "
|
|
546
|
+
case "discover_browse": {
|
|
480
547
|
const domain = args.domain;
|
|
481
548
|
try {
|
|
482
|
-
const result = await
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
579
|
-
|
|
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}`);
|