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.
- package/README.md +202 -193
- package/dist/index.js +410 -192
- package/package.json +45 -45
package/README.md
CHANGED
|
@@ -1,193 +1,202 @@
|
|
|
1
|
-
# mcp-www
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/mcp-www)
|
|
4
|
-
[](https://www.npmjs.com/package/mcp-www)
|
|
4
|
+
[](https://www.npmjs.com/package/mcp-www)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
89
|
+
const normalized = domain.normalize("NFC");
|
|
90
|
+
const warning = detectHomograph(normalized);
|
|
91
|
+
const mcpDomain = `_mcp.${normalized}`;
|
|
40
92
|
try {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
93
|
+
const rawRecords = await resolveTxt(mcpDomain);
|
|
94
|
+
if (rawRecords.length === 0) {
|
|
95
|
+
return { records: [], ...(warning && { homograph_warning: warning }) };
|
|
44
96
|
}
|
|
45
|
-
|
|
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
|
|
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.
|
|
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
|
-
// ---
|
|
100
|
-
async function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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.
|
|
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
|
-
// ---
|
|
173
|
-
function
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
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.
|
|
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
|
|
200
|
-
"Start with
|
|
201
|
-
"
|
|
202
|
-
"
|
|
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: "
|
|
210
|
-
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.",
|
|
211
370
|
inputSchema: {
|
|
212
371
|
type: "object",
|
|
213
372
|
properties: {
|
|
214
373
|
domain: {
|
|
215
374
|
type: "string",
|
|
216
|
-
description: "
|
|
375
|
+
description: "A single domain to look up (e.g., 'example.com')",
|
|
217
376
|
},
|
|
218
|
-
|
|
219
|
-
|
|
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: "
|
|
238
|
-
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.",
|
|
239
388
|
inputSchema: {
|
|
240
389
|
type: "object",
|
|
241
390
|
properties: {
|
|
242
|
-
|
|
243
|
-
type: "
|
|
244
|
-
|
|
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: ["
|
|
396
|
+
required: ["domain"],
|
|
249
397
|
},
|
|
250
398
|
},
|
|
251
399
|
{
|
|
252
|
-
name: "
|
|
253
|
-
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.",
|
|
254
402
|
inputSchema: {
|
|
255
403
|
type: "object",
|
|
256
404
|
properties: {
|
|
257
405
|
domain: {
|
|
258
406
|
type: "string",
|
|
259
|
-
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')",
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 "
|
|
372
|
-
const url = args
|
|
373
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
398
|
-
|
|
586
|
+
else {
|
|
587
|
+
result = await browseDomain(domain);
|
|
399
588
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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: `
|
|
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
|
-
|
|
507
|
-
|
|
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.
|
|
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
|
+
}
|