@rankcli/mcp-server 0.0.8 → 0.0.9
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/dist/{chunk-JSSECRCS.js → chunk-FJ2K7KRV.js} +139 -19
- package/dist/index.js +12 -5
- package/dist/server.d.ts +31 -4
- package/dist/server.js +7 -3
- package/package.json +5 -4
|
@@ -5,15 +5,122 @@ import {
|
|
|
5
5
|
ListToolsRequestSchema
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import { analyzers } from "@rankcli/agent-runtime";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
|
|
9
|
+
// src/fetch-guard.ts
|
|
10
|
+
import { lookup } from "dns/promises";
|
|
11
|
+
import { isIP } from "net";
|
|
12
|
+
var USER_AGENT = "RankCLI/1.0 (+https://rankcli.dev)";
|
|
13
|
+
var MAX_REDIRECTS = 5;
|
|
14
|
+
var TIMEOUT_MS = 15e3;
|
|
15
|
+
function ipv4ToInt(ip) {
|
|
16
|
+
return ip.split(".").reduce((acc, part) => (acc << 8) + Number(part), 0) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
function inV4(ip, cidr) {
|
|
19
|
+
const [base, bits] = cidr.split("/");
|
|
20
|
+
const mask = Number(bits) === 0 ? 0 : ~0 << 32 - Number(bits) >>> 0;
|
|
21
|
+
return (ipv4ToInt(ip) & mask) === (ipv4ToInt(base) & mask);
|
|
22
|
+
}
|
|
23
|
+
var BLOCKED_V4 = [
|
|
24
|
+
"0.0.0.0/8",
|
|
25
|
+
"10.0.0.0/8",
|
|
26
|
+
"100.64.0.0/10",
|
|
27
|
+
"127.0.0.0/8",
|
|
28
|
+
"169.254.0.0/16",
|
|
29
|
+
"172.16.0.0/12",
|
|
30
|
+
"192.0.0.0/24",
|
|
31
|
+
"192.168.0.0/16",
|
|
32
|
+
"198.18.0.0/15",
|
|
33
|
+
"224.0.0.0/4",
|
|
34
|
+
"240.0.0.0/4"
|
|
35
|
+
];
|
|
36
|
+
function isPrivateAddress(ip) {
|
|
37
|
+
const version = isIP(ip);
|
|
38
|
+
if (version === 4) return BLOCKED_V4.some((cidr) => inV4(ip, cidr));
|
|
39
|
+
if (version === 6) {
|
|
40
|
+
const v6 = ip.toLowerCase();
|
|
41
|
+
const mapped = v6.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
42
|
+
if (mapped) return isPrivateAddress(mapped[1]);
|
|
43
|
+
if (v6 === "::" || v6 === "::1") return true;
|
|
44
|
+
const first = parseInt(v6.split(":")[0] || "0", 16);
|
|
45
|
+
if ((first & 65024) === 64512) return true;
|
|
46
|
+
if ((first & 65472) === 65152) return true;
|
|
47
|
+
if ((first & 65280) === 65280) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
async function assertPublicUrl(url) {
|
|
53
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
54
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
55
|
+
throw new Error(`Refusing to fetch ${url.origin}: private network address`);
|
|
56
|
+
}
|
|
57
|
+
const addresses = isIP(host) ? [{ address: host }] : await lookup(host, { all: true, verbatim: true });
|
|
58
|
+
if (addresses.length === 0 || addresses.some((a) => isPrivateAddress(a.address))) {
|
|
59
|
+
throw new Error(`Refusing to fetch ${url.origin}: private network address`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function guardedFetch(rawUrl, options = {}) {
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(rawUrl);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`Invalid URL: ${rawUrl}`);
|
|
68
|
+
}
|
|
69
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
70
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
71
|
+
throw new Error(`Refusing to fetch ${url.protocol} URL - only http(s) is supported`);
|
|
72
|
+
}
|
|
73
|
+
if (options.blockPrivateNetworks) await assertPublicUrl(url);
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
headers: { "User-Agent": USER_AGENT },
|
|
76
|
+
redirect: options.blockPrivateNetworks ? "manual" : "follow",
|
|
77
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
78
|
+
});
|
|
79
|
+
const location = response.headers.get("location");
|
|
80
|
+
if (options.blockPrivateNetworks && response.status >= 300 && response.status < 400 && location) {
|
|
81
|
+
url = new URL(location, url);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`Too many redirects fetching ${rawUrl}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/server.ts
|
|
90
|
+
async function fetchHtml(url, options = {}) {
|
|
91
|
+
const res = await guardedFetch(url, options);
|
|
12
92
|
if (!res.ok) {
|
|
13
93
|
throw new Error(`Fetching ${url} returned ${res.status} ${res.statusText}`);
|
|
14
94
|
}
|
|
15
95
|
return res.text();
|
|
16
96
|
}
|
|
97
|
+
async function fetchRobotsTxt(pageUrl, options = {}) {
|
|
98
|
+
let robotsUrl;
|
|
99
|
+
try {
|
|
100
|
+
robotsUrl = new URL("/robots.txt", pageUrl).href;
|
|
101
|
+
} catch {
|
|
102
|
+
return { note: "robots.txt not checked: invalid URL." };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const res = await guardedFetch(robotsUrl, options);
|
|
106
|
+
if (res.ok) {
|
|
107
|
+
return { content: await res.text(), note: `robots.txt fetched from ${robotsUrl}.` };
|
|
108
|
+
}
|
|
109
|
+
if (res.status >= 500 || res.status === 429) {
|
|
110
|
+
return {
|
|
111
|
+
unknown: true,
|
|
112
|
+
note: `robots.txt returned HTTP ${res.status}, so crawler access is unknown - Google treats a 5xx robots.txt as "disallow everything" until it answers.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { note: `No robots.txt (HTTP ${res.status}) - every crawler is allowed by default.` };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return { unknown: true, note: `robots.txt could not be fetched (${error instanceof Error ? error.message : String(error)}), so crawler access is unknown.` };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function resolveRobotsTxt(url, provided, options) {
|
|
121
|
+
if (provided !== void 0) return { content: provided, note: "robots.txt as provided." };
|
|
122
|
+
return fetchRobotsTxt(url, options);
|
|
123
|
+
}
|
|
17
124
|
var ANALYSIS_TOOLS = [
|
|
18
125
|
{
|
|
19
126
|
name: "seo_analyze",
|
|
@@ -40,7 +147,7 @@ Use this for a complete SEO audit.`,
|
|
|
40
147
|
},
|
|
41
148
|
robotsTxt: {
|
|
42
149
|
type: "string",
|
|
43
|
-
description: "robots.txt content
|
|
150
|
+
description: "robots.txt content (optional - fetched from the site when omitted)"
|
|
44
151
|
}
|
|
45
152
|
},
|
|
46
153
|
required: ["url"]
|
|
@@ -68,11 +175,11 @@ Critical for visibility in ChatGPT, Perplexity, Claude, and Gemini responses.`,
|
|
|
68
175
|
},
|
|
69
176
|
html: {
|
|
70
177
|
type: "string",
|
|
71
|
-
description: "HTML content of the page"
|
|
178
|
+
description: "HTML content of the page (optional - fetched from the URL when omitted)"
|
|
72
179
|
},
|
|
73
180
|
robotsTxt: {
|
|
74
181
|
type: "string",
|
|
75
|
-
description: "robots.txt content"
|
|
182
|
+
description: "robots.txt content (optional - fetched from the site when omitted)"
|
|
76
183
|
}
|
|
77
184
|
},
|
|
78
185
|
required: ["url"]
|
|
@@ -318,7 +425,9 @@ ${result.prioritizedRecommendations.map((r, i) => `${i + 1}. ${r}`).join("\n")}
|
|
|
318
425
|
${BRIDGE_FOOTER}
|
|
319
426
|
`;
|
|
320
427
|
}
|
|
321
|
-
function formatGEOResult(result) {
|
|
428
|
+
function formatGEOResult(result, robots) {
|
|
429
|
+
const access = robots.unknown ? `| \u2754 Unknown | ${robots.note} |` : `| \u2705 Allowed | ${result.aiCrawlerAccess.allowedCrawlers.join(", ") || "None"} |
|
|
430
|
+
| \u274C Blocked | ${result.aiCrawlerAccess.blockedCrawlers.join(", ") || "None"} |`;
|
|
322
431
|
return `# GEO Analysis (AI Search Optimization)
|
|
323
432
|
|
|
324
433
|
**Score:** ${result.score}/100
|
|
@@ -327,8 +436,9 @@ function formatGEOResult(result) {
|
|
|
327
436
|
|
|
328
437
|
| Status | Crawlers |
|
|
329
438
|
|--------|----------|
|
|
330
|
-
|
|
331
|
-
|
|
439
|
+
${access}
|
|
440
|
+
|
|
441
|
+
_${robots.note}_
|
|
332
442
|
|
|
333
443
|
**Server-Side Rendered:** ${result.aiCrawlerAccess.serverSideRendered ? "\u2705 Yes" : "\u274C No"}
|
|
334
444
|
**JS Rendering Required:** ${result.aiCrawlerAccess.jsRenderingRequired ? "\u26A0\uFE0F Yes (AI crawlers may not see content)" : "\u2705 No"}
|
|
@@ -539,19 +649,27 @@ Allow: /
|
|
|
539
649
|
\`\`\`
|
|
540
650
|
`;
|
|
541
651
|
}
|
|
542
|
-
async function handleAnalysisTool(name, args) {
|
|
652
|
+
async function handleAnalysisTool(name, args, options = {}) {
|
|
543
653
|
switch (name) {
|
|
544
654
|
case "seo_analyze": {
|
|
545
655
|
const { url, html: providedHtml, robotsTxt } = args;
|
|
546
|
-
const html =
|
|
547
|
-
|
|
548
|
-
|
|
656
|
+
const [html, robots] = await Promise.all([
|
|
657
|
+
providedHtml ?? fetchHtml(url, options),
|
|
658
|
+
resolveRobotsTxt(url, robotsTxt, options)
|
|
659
|
+
]);
|
|
660
|
+
const result = await analyzers.analyzeComprehensive(html, url, { robotsTxt: robots.content });
|
|
661
|
+
return { content: [{ type: "text", text: `${formatComprehensiveResult(result)}
|
|
662
|
+
_${robots.note}_
|
|
663
|
+
` }] };
|
|
549
664
|
}
|
|
550
665
|
case "seo_geo_check": {
|
|
551
666
|
const { url, html: providedHtml, robotsTxt } = args;
|
|
552
|
-
const html =
|
|
553
|
-
|
|
554
|
-
|
|
667
|
+
const [html, robots] = await Promise.all([
|
|
668
|
+
providedHtml ?? fetchHtml(url, options),
|
|
669
|
+
resolveRobotsTxt(url, robotsTxt, options)
|
|
670
|
+
]);
|
|
671
|
+
const result = await analyzers.analyzeGEO(html, url, robots.content);
|
|
672
|
+
return { content: [{ type: "text", text: formatGEOResult(result, robots) }] };
|
|
555
673
|
}
|
|
556
674
|
case "seo_robots_ai": {
|
|
557
675
|
const { robotsTxt } = args;
|
|
@@ -652,7 +770,7 @@ ${Object.entries(headers).map(([k, v]) => `**${k}:**
|
|
|
652
770
|
return void 0;
|
|
653
771
|
}
|
|
654
772
|
}
|
|
655
|
-
function createAnalysisServer() {
|
|
773
|
+
function createAnalysisServer(options = {}) {
|
|
656
774
|
const server = new Server(
|
|
657
775
|
{ name: "rankcli", version: "0.0.1" },
|
|
658
776
|
{ capabilities: { tools: {} } }
|
|
@@ -663,7 +781,7 @@ function createAnalysisServer() {
|
|
|
663
781
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
664
782
|
const { name, arguments: args } = request.params;
|
|
665
783
|
try {
|
|
666
|
-
const result = await handleAnalysisTool(name, args);
|
|
784
|
+
const result = await handleAnalysisTool(name, args, options);
|
|
667
785
|
if (result) return result;
|
|
668
786
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
669
787
|
} catch (error) {
|
|
@@ -677,7 +795,9 @@ function createAnalysisServer() {
|
|
|
677
795
|
}
|
|
678
796
|
|
|
679
797
|
export {
|
|
798
|
+
isPrivateAddress,
|
|
680
799
|
fetchHtml,
|
|
800
|
+
fetchRobotsTxt,
|
|
681
801
|
ANALYSIS_TOOLS,
|
|
682
802
|
handleAnalysisTool,
|
|
683
803
|
createAnalysisServer
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ANALYSIS_TOOLS,
|
|
4
4
|
handleAnalysisTool
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-FJ2K7KRV.js";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -118,6 +118,12 @@ function clearPendingConnect() {
|
|
|
118
118
|
localConfig.delete("pendingConnectId");
|
|
119
119
|
localConfig.delete("pendingConnectUrl");
|
|
120
120
|
localConfig.delete("pendingConnectExpiresAt");
|
|
121
|
+
localConfig.delete("pendingConnectCode");
|
|
122
|
+
}
|
|
123
|
+
function codeLine(userCode) {
|
|
124
|
+
return userCode ? `
|
|
125
|
+
|
|
126
|
+
Approve only if the page shows this code: ${userCode}` : "";
|
|
121
127
|
}
|
|
122
128
|
async function connectToRankCLI() {
|
|
123
129
|
const existingKey = localConfig.get("apiKey");
|
|
@@ -144,7 +150,7 @@ async function connectToRankCLI() {
|
|
|
144
150
|
return {
|
|
145
151
|
text: `Still waiting for approval. Open this link if you haven't yet, then run rankcli_connect again:
|
|
146
152
|
|
|
147
|
-
${pendingUrl}
|
|
153
|
+
${pendingUrl}${codeLine(localConfig.get("pendingConnectCode"))}
|
|
148
154
|
|
|
149
155
|
Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
|
|
150
156
|
};
|
|
@@ -161,17 +167,18 @@ Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
|
|
|
161
167
|
isError: true
|
|
162
168
|
};
|
|
163
169
|
}
|
|
164
|
-
const { connectId, url, expiresInSeconds } = startData;
|
|
170
|
+
const { connectId, userCode, url, expiresInSeconds } = startData;
|
|
165
171
|
const expiresMinutes = Math.round(expiresInSeconds / 60);
|
|
166
172
|
localConfig.set("pendingConnectId", connectId);
|
|
167
173
|
localConfig.set("pendingConnectUrl", url);
|
|
168
174
|
localConfig.set("pendingConnectExpiresAt", Date.now() + expiresInSeconds * 1e3);
|
|
175
|
+
if (userCode) localConfig.set("pendingConnectCode", userCode);
|
|
169
176
|
const supportsUrlElicitation = !!server.getClientCapabilities()?.elicitation?.url;
|
|
170
177
|
if (supportsUrlElicitation) {
|
|
171
178
|
try {
|
|
172
179
|
const result = await server.elicitInput({
|
|
173
180
|
mode: "url",
|
|
174
|
-
message:
|
|
181
|
+
message: `Connect your RankCLI account to enable GitHub auto-fix PRs, scheduled monitoring, and a dashboard with audit history.${codeLine(userCode)}`,
|
|
175
182
|
url,
|
|
176
183
|
elicitationId: connectId
|
|
177
184
|
});
|
|
@@ -195,7 +202,7 @@ Expires in ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"}.`
|
|
|
195
202
|
return {
|
|
196
203
|
text: `Open this link to connect your RankCLI account:
|
|
197
204
|
|
|
198
|
-
${url}
|
|
205
|
+
${url}${codeLine(userCode)}
|
|
199
206
|
|
|
200
207
|
Then run rankcli_connect again to finish (link expires in ${expiresMinutes} minutes).`
|
|
201
208
|
};
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
|
|
4
|
+
interface FetchGuardOptions {
|
|
5
|
+
/** Refuse loopback, private, link-local, CGNAT and metadata addresses. */
|
|
6
|
+
blockPrivateNetworks?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Loopback, private, link-local, CGNAT, multicast/reserved - anything not on the public internet. */
|
|
9
|
+
declare function isPrivateAddress(ip: string): boolean;
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Shared, stateless SEO/GEO analysis tools - the part of the RankCLI MCP
|
|
6
13
|
* server that's safe to run anywhere, local stdio process or a shared
|
|
@@ -11,19 +18,39 @@ import { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
11
18
|
* via local file storage.
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
/** Options for the analysis tools. Everything defaults off, for local stdio use. */
|
|
22
|
+
type AnalysisServerOptions = FetchGuardOptions;
|
|
23
|
+
declare function fetchHtml(url: string, options?: AnalysisServerOptions): Promise<string>;
|
|
24
|
+
interface RobotsTxtResult {
|
|
25
|
+
/** robots.txt body when the site served one (2xx). */
|
|
26
|
+
content?: string;
|
|
27
|
+
/** One line for the report: what was found, or why crawler access is unknown. */
|
|
28
|
+
note: string;
|
|
29
|
+
/** robots.txt couldn't be read (5xx, 429, network) - access is not known. */
|
|
30
|
+
unknown?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The site's own robots.txt. seo_geo_check / seo_analyze used to skip this
|
|
34
|
+
* unless the caller pasted robots.txt in, and then reported every AI
|
|
35
|
+
* crawler as "allowed" - even on sites that block them all.
|
|
36
|
+
*/
|
|
37
|
+
declare function fetchRobotsTxt(pageUrl: string, options?: AnalysisServerOptions): Promise<RobotsTxtResult>;
|
|
15
38
|
declare const ANALYSIS_TOOLS: Tool[];
|
|
16
39
|
/**
|
|
17
40
|
* Handles a single analysis tool call. Returns undefined if `name` isn't
|
|
18
41
|
* one of ANALYSIS_TOOLS, so callers (index.ts's stdio server) can fall
|
|
19
42
|
* through to their own additional tools.
|
|
20
43
|
*/
|
|
21
|
-
declare function handleAnalysisTool(name: string, args: Record<string, unknown> | undefined): Promise<CallToolResult | undefined>;
|
|
44
|
+
declare function handleAnalysisTool(name: string, args: Record<string, unknown> | undefined, options?: AnalysisServerOptions): Promise<CallToolResult | undefined>;
|
|
22
45
|
/**
|
|
23
46
|
* A fresh Server exposing only the stateless analysis tools - used by the
|
|
24
47
|
* remote HTTP endpoint, one instance per request (see packages/audit-worker),
|
|
25
48
|
* matching the MCP SDK's own stateless-server example.
|
|
49
|
+
*
|
|
50
|
+
* A shared deployment should pass { blockPrivateNetworks: true } so callers
|
|
51
|
+
* can't point the fetching tools at internal addresses. Off by default: the
|
|
52
|
+
* local stdio server auditing http://localhost:3000 is a feature.
|
|
26
53
|
*/
|
|
27
|
-
declare function createAnalysisServer(): Server;
|
|
54
|
+
declare function createAnalysisServer(options?: AnalysisServerOptions): Server;
|
|
28
55
|
|
|
29
|
-
export { ANALYSIS_TOOLS, createAnalysisServer, fetchHtml, handleAnalysisTool };
|
|
56
|
+
export { ANALYSIS_TOOLS, type AnalysisServerOptions, type FetchGuardOptions, type RobotsTxtResult, createAnalysisServer, fetchHtml, fetchRobotsTxt, handleAnalysisTool, isPrivateAddress };
|
package/dist/server.js
CHANGED
|
@@ -2,11 +2,15 @@ import {
|
|
|
2
2
|
ANALYSIS_TOOLS,
|
|
3
3
|
createAnalysisServer,
|
|
4
4
|
fetchHtml,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
fetchRobotsTxt,
|
|
6
|
+
handleAnalysisTool,
|
|
7
|
+
isPrivateAddress
|
|
8
|
+
} from "./chunk-FJ2K7KRV.js";
|
|
7
9
|
export {
|
|
8
10
|
ANALYSIS_TOOLS,
|
|
9
11
|
createAnalysisServer,
|
|
10
12
|
fetchHtml,
|
|
11
|
-
|
|
13
|
+
fetchRobotsTxt,
|
|
14
|
+
handleAnalysisTool,
|
|
15
|
+
isPrivateAddress
|
|
12
16
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankcli/mcp-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"mcpName": "io.github.integrallis/rankcli-mcp-server",
|
|
5
5
|
"description": "Free, local SEO + GEO (AI-search-citation) analysis for AI assistants - audits GEO/AI-crawler access, Core Web Vitals, structured data, security headers, mobile, and images with no signup or API key.",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"build": "tsup src/index.ts src/server.ts --format esm --dts --clean",
|
|
24
24
|
"dev": "tsup src/index.ts src/server.ts --format esm --watch",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"test": "
|
|
26
|
+
"test": "vitest run",
|
|
27
27
|
"prepublishOnly": "npm run build"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
|
@@ -51,12 +51,13 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
54
|
-
"@rankcli/agent-runtime": "^0.0.
|
|
54
|
+
"@rankcli/agent-runtime": "^0.0.21",
|
|
55
55
|
"conf": "^12.0.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"tsup": "^8.0.0",
|
|
59
|
-
"typescript": "^5.4.0"
|
|
59
|
+
"typescript": "^5.4.0",
|
|
60
|
+
"vitest": "^1.6.0"
|
|
60
61
|
},
|
|
61
62
|
"files": [
|
|
62
63
|
"dist"
|