@refreshagent/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/package.json +29 -0
  3. package/src/index.js +299 -0
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Refresh Agent MCP
2
+
3
+ Connect Claude Code to Refresh Agent without exposing Google setup details.
4
+
5
+ ```bash
6
+ claude mcp add refreshagent npx -y @refreshagent/mcp --key=ra_live_...
7
+ ```
8
+
9
+ The bridge reads `--key`, `REFRESHAGENT_API_KEY`, or `~/.config/refreshagent/.env`, and proxies Claude tool calls to the Refresh Agent API.
10
+
11
+ The `live-search-console-data` skill can create that local config file automatically through the browser login flow.
12
+
13
+ For clients that accept an MCP JSON configuration:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "refreshagent": {
19
+ "command": "npx",
20
+ "args": ["-y", "@refreshagent/mcp"],
21
+ "env": { "REFRESHAGENT_API_KEY": "YOUR_REFRESHAGENT_API_KEY" }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ Requires Node.js 18 or newer and a RefreshAgent account with Google data connected.
28
+ Tools: `list_connected_sites`, `check_website_traffic`, `get_keyword_rankings`.
29
+ Pricing and data-access limits: https://refreshagent.com/plans.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@refreshagent/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Refresh Agent MCP bridge for Claude Code",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "refreshagent-mcp": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "UNLICENSED",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/dunctk/seo-ai-agent-web.git",
20
+ "directory": "mcp-package"
21
+ },
22
+ "mcpName": "io.github.dunctk/refreshagent",
23
+ "scripts": {
24
+ "test": "node --test test/*.test.cjs"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }
package/src/index.js ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ const args = parseArgs(process.argv.slice(2));
8
+ const config = readRefreshAgentConfig();
9
+ const apiKey = args.key || process.env.REFRESHAGENT_API_KEY || config.REFRESHAGENT_API_KEY || "";
10
+ const baseUrl = (args["base-url"] || process.env.REFRESHAGENT_BASE_URL || config.REFRESHAGENT_BASE_URL || "https://refreshagent.com").replace(/\/+$/, "");
11
+
12
+ let inputBuffer = Buffer.alloc(0);
13
+
14
+ process.stdin.resume();
15
+ process.stdin.on("data", (chunk) => {
16
+ inputBuffer = Buffer.concat([inputBuffer, chunk]);
17
+ drainMessages();
18
+ });
19
+
20
+ function parseArgs(argv) {
21
+ const parsed = {};
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i];
24
+ if (!arg.startsWith("--")) continue;
25
+ const trimmed = arg.slice(2);
26
+ const eq = trimmed.indexOf("=");
27
+ if (eq >= 0) {
28
+ parsed[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
29
+ } else {
30
+ parsed[trimmed] = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
31
+ }
32
+ }
33
+ return parsed;
34
+ }
35
+
36
+ function readRefreshAgentConfig() {
37
+ const file = path.join(os.homedir(), ".config", "refreshagent", ".env");
38
+ const parsed = {};
39
+ let text = "";
40
+ try {
41
+ text = fs.readFileSync(file, "utf8");
42
+ } catch (_) {
43
+ return parsed;
44
+ }
45
+
46
+ for (const line of text.split(/\r?\n/)) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
49
+ const [rawKey, ...rawValue] = trimmed.split("=");
50
+ const key = rawKey.trim();
51
+ const value = rawValue.join("=").trim().replace(/^['"]|['"]$/g, "");
52
+ if (key) parsed[key] = value;
53
+ }
54
+
55
+ return parsed;
56
+ }
57
+
58
+ function drainMessages() {
59
+ while (true) {
60
+ const lineEnd = inputBuffer.indexOf("\n");
61
+ if (lineEnd < 0) return;
62
+ const body = inputBuffer.slice(0, lineEnd).toString("utf8").trim();
63
+ inputBuffer = inputBuffer.slice(lineEnd + 1);
64
+ if (!body) continue;
65
+
66
+ let message;
67
+ try {
68
+ message = JSON.parse(body);
69
+ } catch (error) {
70
+ writeResponse(null, null, rpcError(-32700, `Invalid JSON: ${error.message}`));
71
+ continue;
72
+ }
73
+
74
+ handleMessage(message).catch((error) => {
75
+ writeResponse(message.id, null, rpcError(-32603, error.message || String(error)));
76
+ });
77
+ }
78
+ }
79
+
80
+ async function handleMessage(message) {
81
+ if (!message || !message.method) return;
82
+
83
+ if (message.method.startsWith("notifications/")) return;
84
+
85
+ switch (message.method) {
86
+ case "initialize":
87
+ writeResponse(message.id, {
88
+ protocolVersion: message.params?.protocolVersion || "2024-11-05",
89
+ capabilities: { tools: {} },
90
+ serverInfo: { name: "Refresh Agent", version: require("../package.json").version }
91
+ });
92
+ return;
93
+ case "ping":
94
+ writeResponse(message.id, {});
95
+ return;
96
+ case "tools/list":
97
+ writeResponse(message.id, { tools: toolDefinitions() });
98
+ return;
99
+ case "tools/call": {
100
+ const name = message.params?.name;
101
+ const toolArgs = message.params?.arguments || {};
102
+ const result = await callTool(name, toolArgs);
103
+ writeResponse(message.id, result);
104
+ return;
105
+ }
106
+ default:
107
+ writeResponse(message.id, null, rpcError(-32601, `Unknown method: ${message.method}`));
108
+ }
109
+ }
110
+
111
+ function writeResponse(id, result, error = null) {
112
+ const payload = JSON.stringify({
113
+ jsonrpc: "2.0",
114
+ id,
115
+ ...(error ? { error } : { result })
116
+ });
117
+ process.stdout.write(`${payload}\n`);
118
+ }
119
+
120
+ function rpcError(code, message) {
121
+ return { code, message };
122
+ }
123
+
124
+ function toolDefinitions() {
125
+ return [
126
+ {
127
+ name: "list_connected_sites",
128
+ description: "List the websites Refresh Agent can access for this account.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {},
132
+ additionalProperties: false
133
+ }
134
+ },
135
+ {
136
+ name: "check_website_traffic",
137
+ description: "Check recent website traffic and top pages from connected Google Analytics and Search Console data.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ date_range: {
142
+ type: "string",
143
+ enum: ["7d", "30d", "90d"],
144
+ description: "Time window to check. Defaults to 7d."
145
+ },
146
+ site_url: {
147
+ type: "string",
148
+ description: "Optional Search Console site URL. If omitted, Refresh Agent uses the first connected site."
149
+ },
150
+ property_id: {
151
+ type: "string",
152
+ description: "Optional GA4 property ID. If omitted, Refresh Agent uses the first connected GA4 property."
153
+ }
154
+ },
155
+ additionalProperties: false
156
+ }
157
+ },
158
+ {
159
+ name: "get_keyword_rankings",
160
+ description: "Get top Search Console queries with clicks, impressions, CTR, and average position.",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ date_range: {
165
+ type: "string",
166
+ enum: ["7d", "30d", "90d"],
167
+ description: "Time window to check. Defaults to 30d."
168
+ },
169
+ site_url: {
170
+ type: "string",
171
+ description: "Optional Search Console site URL."
172
+ }
173
+ },
174
+ additionalProperties: false
175
+ }
176
+ }
177
+ ];
178
+ }
179
+
180
+ async function callTool(name, toolArgs) {
181
+ if (!apiKey) {
182
+ return toolError("Refresh Agent secure key missing. In Claude Code, run the live-search-console-data skill once; it will open Google login and save the key locally.");
183
+ }
184
+
185
+ switch (name) {
186
+ case "list_connected_sites":
187
+ return toolText(await refreshJson("/api/v1/clients"));
188
+ case "check_website_traffic":
189
+ return checkWebsiteTraffic(toolArgs);
190
+ case "get_keyword_rankings":
191
+ return getKeywordRankings(toolArgs);
192
+ default:
193
+ return toolError(`Unknown Refresh Agent tool: ${name}`);
194
+ }
195
+ }
196
+
197
+ async function checkWebsiteTraffic(toolArgs) {
198
+ const dateRange = toolArgs.date_range || "7d";
199
+ const defaults = await connectedDefaults();
200
+ const siteUrl = toolArgs.site_url || defaults.site_url;
201
+ const propertyId = toolArgs.property_id || defaults.property_id;
202
+
203
+ const output = {
204
+ date_range: dateRange,
205
+ site_url: siteUrl || null,
206
+ property_id: propertyId || null,
207
+ traffic_summary: null,
208
+ top_pages: null,
209
+ notes: []
210
+ };
211
+
212
+ if (propertyId) {
213
+ output.traffic_summary = await refreshJson("/api/v1/ga4/summary", { property_id: propertyId, date_range: dateRange });
214
+ } else {
215
+ output.notes.push("No connected GA4 property was found.");
216
+ }
217
+
218
+ if (siteUrl) {
219
+ output.top_pages = await refreshJson("/api/v1/sc/pages", { site_url: siteUrl, date_range: dateRange });
220
+ } else {
221
+ output.notes.push("No connected Search Console site was found.");
222
+ }
223
+
224
+ return toolText(output);
225
+ }
226
+
227
+ async function getKeywordRankings(toolArgs) {
228
+ const dateRange = toolArgs.date_range || "30d";
229
+ const defaults = await connectedDefaults();
230
+ const siteUrl = toolArgs.site_url || defaults.site_url;
231
+ if (!siteUrl) return toolError("No connected Search Console site was found.");
232
+
233
+ const rankings = await refreshJson("/api/v1/sc/query", { site_url: siteUrl, date_range: dateRange });
234
+ return toolText({ date_range: dateRange, site_url: siteUrl, rankings });
235
+ }
236
+
237
+ async function connectedDefaults() {
238
+ const clients = await refreshJson("/api/v1/clients");
239
+ const list = Array.isArray(clients) ? clients : clients.clients || clients.sites || [];
240
+ const firstGsc = list.find((site) => site.gsc_site_url || site.site_url);
241
+ const firstGa4 = list.find((site) => site.ga4_property_id || site.property_id);
242
+ return {
243
+ site_url: firstGsc?.gsc_site_url || firstGsc?.site_url || null,
244
+ property_id: firstGa4?.ga4_property_id || firstGa4?.property_id || null
245
+ };
246
+ }
247
+
248
+ async function refreshJson(path, params = {}) {
249
+ const url = new URL(`${baseUrl}${path}`);
250
+ for (const [key, value] of Object.entries(params)) {
251
+ if (value !== undefined && value !== null && value !== "") {
252
+ url.searchParams.set(key, String(value));
253
+ }
254
+ }
255
+
256
+ const response = await fetch(url, {
257
+ headers: {
258
+ "Accept": "application/json",
259
+ "X-API-Key": apiKey
260
+ }
261
+ });
262
+
263
+ const text = await response.text();
264
+ let payload;
265
+ try {
266
+ payload = text ? JSON.parse(text) : {};
267
+ } catch (_) {
268
+ payload = { raw: text };
269
+ }
270
+
271
+ if (!response.ok) {
272
+ throw new Error(payload.error || payload.message || `Refresh Agent request failed with ${response.status}`);
273
+ }
274
+
275
+ return payload;
276
+ }
277
+
278
+ function toolText(data) {
279
+ return {
280
+ content: [
281
+ {
282
+ type: "text",
283
+ text: JSON.stringify(data, null, 2)
284
+ }
285
+ ]
286
+ };
287
+ }
288
+
289
+ function toolError(message) {
290
+ return {
291
+ isError: true,
292
+ content: [
293
+ {
294
+ type: "text",
295
+ text: message
296
+ }
297
+ ]
298
+ };
299
+ }