@metrone-io/mcp 1.0.0 → 1.0.2
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 +57 -0
- package/dist/index.js +234 -130
- package/dist/tools.d.ts +78 -0
- package/dist/tools.js +195 -0
- package/package.json +28 -2
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @metrone-io/mcp
|
|
2
|
+
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server for [Metrone](https://metrone.io) analytics. Gives AI agents (Claude, GPT, Cursor, etc.) native access to your analytics data.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @metrone-io/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Add to your MCP client configuration:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"metrone": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "@metrone-io/mcp"],
|
|
21
|
+
"env": {
|
|
22
|
+
"METRONE_API_KEY": "metrone_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Optionally set `METRONE_ENDPOINT` to override the default API base URL (`https://api.metrone.io`).
|
|
30
|
+
|
|
31
|
+
## Available Tools
|
|
32
|
+
|
|
33
|
+
| Tool | Description |
|
|
34
|
+
|------|-------------|
|
|
35
|
+
| `metrone_get_stats` | Aggregate analytics (pageviews, visitors, bounce rate) |
|
|
36
|
+
| `metrone_get_events` | Recent events with filtering |
|
|
37
|
+
| `metrone_get_pages` | Top pages by views |
|
|
38
|
+
| `metrone_get_sources` | Traffic sources breakdown |
|
|
39
|
+
| `metrone_get_live` | Current live visitors |
|
|
40
|
+
| `metrone_track_event` | Track a custom event |
|
|
41
|
+
| `metrone_track_ai_call` | Track an AI call or chat session |
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
Ask your AI agent:
|
|
46
|
+
|
|
47
|
+
> "How many pageviews did we get this week?"
|
|
48
|
+
> "What are our top traffic sources?"
|
|
49
|
+
> "Track a conversion event for user signup"
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
[metrone.io/docs](https://metrone.io/docs)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
[MIT](../../LICENSE)
|
package/dist/index.js
CHANGED
|
@@ -1,153 +1,257 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
process.exit(1);
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
|
|
8
|
+
// src/tools.ts
|
|
9
|
+
function num(v) {
|
|
10
|
+
return typeof v === "number" && Number.isFinite(v) ? String(v) : void 0;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (params) {
|
|
16
|
-
for (const [k, v] of Object.entries(params)) {
|
|
17
|
-
if (v !== void 0 && v !== "") url.searchParams.set(k, v);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const res = await fetch(url.toString(), {
|
|
21
|
-
headers: { "X-Api-Key": API_KEY }
|
|
22
|
-
});
|
|
23
|
-
if (!res.ok) {
|
|
24
|
-
const body = await res.text();
|
|
25
|
-
throw new Error(`Metrone API error ${res.status}: ${body}`);
|
|
26
|
-
}
|
|
27
|
-
return res.json();
|
|
12
|
+
function str(v) {
|
|
13
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
28
14
|
}
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
|
|
34
|
-
body: JSON.stringify(body)
|
|
35
|
-
});
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
const text = await res.text();
|
|
38
|
-
throw new Error(`Metrone API error ${res.status}: ${text}`);
|
|
15
|
+
function query(entries) {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
18
|
+
if (v !== void 0) out[k] = v;
|
|
39
19
|
}
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
function textResult(data) {
|
|
43
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
20
|
+
return out;
|
|
44
21
|
}
|
|
45
|
-
var
|
|
46
|
-
server.tool(
|
|
47
|
-
"metrone_get_stats",
|
|
22
|
+
var TOOLS = [
|
|
48
23
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
24
|
+
name: "metrone_get_stats",
|
|
25
|
+
description: "Get aggregated analytics statistics (events, pageviews, visitors, sessions, conversion rate) for a time period.",
|
|
26
|
+
scope: "read",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
days: { type: "number", description: "Look-back window in days (default 30)" },
|
|
31
|
+
from: { type: "string", description: "ISO 8601 start date (overrides days)" },
|
|
32
|
+
to: { type: "string", description: "ISO 8601 end date" }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
buildRequest: (args) => ({
|
|
36
|
+
method: "GET",
|
|
37
|
+
path: "/v1/api/stats",
|
|
38
|
+
query: query({ days: num(args.days), from: str(args.from), to: str(args.to) })
|
|
39
|
+
})
|
|
52
40
|
},
|
|
53
|
-
async ({ days, from, to }) => {
|
|
54
|
-
const params = {};
|
|
55
|
-
if (days !== void 0) params.days = String(days);
|
|
56
|
-
if (from) params.from = from;
|
|
57
|
-
if (to) params.to = to;
|
|
58
|
-
return textResult(await apiGet("/v1/api/stats", params));
|
|
59
|
-
}
|
|
60
|
-
);
|
|
61
|
-
server.tool(
|
|
62
|
-
"metrone_get_events",
|
|
63
41
|
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
42
|
+
name: "metrone_get_events",
|
|
43
|
+
description: "Query raw analytics events with optional filters for event type and source.",
|
|
44
|
+
scope: "read",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
days: { type: "number" },
|
|
49
|
+
limit: { type: "number" },
|
|
50
|
+
event_type: { type: "string" },
|
|
51
|
+
source: { type: "string" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
buildRequest: (args) => ({
|
|
55
|
+
method: "GET",
|
|
56
|
+
path: "/v1/api/events",
|
|
57
|
+
query: query({
|
|
58
|
+
days: num(args.days),
|
|
59
|
+
limit: num(args.limit),
|
|
60
|
+
event_type: str(args.event_type),
|
|
61
|
+
source: str(args.source)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
68
64
|
},
|
|
69
|
-
async ({ days, limit, event_type, source }) => {
|
|
70
|
-
const params = {};
|
|
71
|
-
if (days !== void 0) params.days = String(days);
|
|
72
|
-
if (limit !== void 0) params.limit = String(limit);
|
|
73
|
-
if (event_type) params.event_type = event_type;
|
|
74
|
-
if (source) params.source = source;
|
|
75
|
-
return textResult(await apiGet("/v1/api/events", params));
|
|
76
|
-
}
|
|
77
|
-
);
|
|
78
|
-
server.tool(
|
|
79
|
-
"metrone_get_pages",
|
|
80
65
|
{
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
name: "metrone_get_pages",
|
|
67
|
+
description: "Get top pages by views and sessions.",
|
|
68
|
+
scope: "read",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
days: { type: "number" },
|
|
73
|
+
limit: { type: "number" }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
buildRequest: (args) => ({
|
|
77
|
+
method: "GET",
|
|
78
|
+
path: "/v1/api/pages",
|
|
79
|
+
query: query({ days: num(args.days), limit: num(args.limit) })
|
|
80
|
+
})
|
|
83
81
|
},
|
|
84
|
-
async ({ days, limit }) => {
|
|
85
|
-
const params = {};
|
|
86
|
-
if (days !== void 0) params.days = String(days);
|
|
87
|
-
if (limit !== void 0) params.limit = String(limit);
|
|
88
|
-
return textResult(await apiGet("/v1/api/pages", params));
|
|
89
|
-
}
|
|
90
|
-
);
|
|
91
|
-
server.tool(
|
|
92
|
-
"metrone_get_sources",
|
|
93
|
-
{ days: z.number().optional() },
|
|
94
|
-
async ({ days }) => {
|
|
95
|
-
const params = {};
|
|
96
|
-
if (days !== void 0) params.days = String(days);
|
|
97
|
-
return textResult(await apiGet("/v1/api/sources", params));
|
|
98
|
-
}
|
|
99
|
-
);
|
|
100
|
-
server.tool(
|
|
101
|
-
"metrone_get_live",
|
|
102
|
-
{},
|
|
103
|
-
async (_args) => textResult(await apiGet("/v1/api/live"))
|
|
104
|
-
);
|
|
105
|
-
server.tool(
|
|
106
|
-
"metrone_track_event",
|
|
107
82
|
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
83
|
+
name: "metrone_get_sources",
|
|
84
|
+
description: "Get traffic source and referrer breakdown.",
|
|
85
|
+
scope: "read",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
days: { type: "number" }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
buildRequest: (args) => ({
|
|
93
|
+
method: "GET",
|
|
94
|
+
path: "/v1/api/sources",
|
|
95
|
+
query: query({ days: num(args.days) })
|
|
96
|
+
})
|
|
113
97
|
},
|
|
114
|
-
async ({ event_type, source, event_name, page_url, properties }) => {
|
|
115
|
-
const payload = {
|
|
116
|
-
api_key: API_KEY,
|
|
117
|
-
event_type,
|
|
118
|
-
source: source ?? "assistant"
|
|
119
|
-
};
|
|
120
|
-
if (event_name) payload.event_name = event_name;
|
|
121
|
-
if (page_url) payload.page_url = page_url;
|
|
122
|
-
if (properties) payload.properties = properties;
|
|
123
|
-
return textResult(await apiPost("/v1/api/events", payload));
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
server.tool(
|
|
127
|
-
"metrone_track_ai_call",
|
|
128
98
|
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
properties: z.record(z.string(), z.any()).optional()
|
|
99
|
+
name: "metrone_get_live",
|
|
100
|
+
description: "Get real-time stats: active visitors and today\u2019s totals.",
|
|
101
|
+
scope: "read",
|
|
102
|
+
inputSchema: { type: "object", properties: {} },
|
|
103
|
+
buildRequest: () => ({ method: "GET", path: "/v1/api/live", query: {} })
|
|
135
104
|
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
105
|
+
{
|
|
106
|
+
name: "metrone_track_event",
|
|
107
|
+
description: "Track a custom analytics event.",
|
|
108
|
+
scope: "write",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
event_type: { type: "string" },
|
|
113
|
+
source: { type: "string", description: 'Event source (defaults to "assistant")' },
|
|
114
|
+
event_name: { type: "string" },
|
|
115
|
+
page_url: { type: "string" },
|
|
116
|
+
properties: { type: "object" }
|
|
117
|
+
},
|
|
118
|
+
required: ["event_type"]
|
|
119
|
+
},
|
|
120
|
+
buildRequest: (args) => {
|
|
121
|
+
const body = {
|
|
122
|
+
event_type: args.event_type,
|
|
123
|
+
source: str(args.source) ?? "assistant"
|
|
124
|
+
};
|
|
125
|
+
if (str(args.event_name)) body.event_name = args.event_name;
|
|
126
|
+
if (str(args.page_url)) body.page_url = args.page_url;
|
|
127
|
+
if (args.properties && typeof args.properties === "object") body.properties = args.properties;
|
|
128
|
+
return { method: "POST", path: "/v1/api/events", body };
|
|
148
129
|
}
|
|
149
|
-
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "metrone_track_ai_call",
|
|
133
|
+
description: "Track an AI call event (voice / chat / assistant interaction).",
|
|
134
|
+
scope: "write",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
call_id: { type: "string" },
|
|
139
|
+
provider: { type: "string" },
|
|
140
|
+
source: { type: "string", enum: ["voice", "chat", "assistant"] },
|
|
141
|
+
duration: { type: "number", description: "Duration in seconds" },
|
|
142
|
+
intent: { type: "string" },
|
|
143
|
+
outcome: { type: "string" },
|
|
144
|
+
properties: { type: "object" }
|
|
145
|
+
},
|
|
146
|
+
required: ["call_id", "provider"]
|
|
147
|
+
},
|
|
148
|
+
buildRequest: (args) => {
|
|
149
|
+
const body = {
|
|
150
|
+
event_type: "ai_call",
|
|
151
|
+
source: str(args.source) ?? "voice",
|
|
152
|
+
ai_call_id: args.call_id,
|
|
153
|
+
ai_provider: args.provider
|
|
154
|
+
};
|
|
155
|
+
if (typeof args.duration === "number") body.ai_duration_sec = args.duration;
|
|
156
|
+
if (str(args.intent)) body.ai_intent = args.intent;
|
|
157
|
+
const props = args.properties && typeof args.properties === "object" ? args.properties : void 0;
|
|
158
|
+
if (props || str(args.outcome)) {
|
|
159
|
+
body.properties = { ...props, ...str(args.outcome) ? { outcome: args.outcome } : {} };
|
|
160
|
+
}
|
|
161
|
+
return { method: "POST", path: "/v1/api/events", body };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
];
|
|
165
|
+
var TOOLS_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
166
|
+
function listTools() {
|
|
167
|
+
return TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
|
|
168
|
+
}
|
|
169
|
+
function getTool(name) {
|
|
170
|
+
return TOOLS_BY_NAME.get(name);
|
|
171
|
+
}
|
|
172
|
+
var ToolValidationError = class extends Error {
|
|
173
|
+
constructor(message) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.name = "ToolValidationError";
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
function validateArgs(tool, args) {
|
|
179
|
+
const required = tool.inputSchema.required ?? [];
|
|
180
|
+
for (const field of required) {
|
|
181
|
+
const v = args[field];
|
|
182
|
+
if (v === void 0 || v === null || typeof v === "string" && v.length === 0) {
|
|
183
|
+
throw new ToolValidationError(`Missing required argument: ${field}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function dispatch(toolName, args, executor) {
|
|
188
|
+
const tool = getTool(toolName);
|
|
189
|
+
if (!tool) {
|
|
190
|
+
throw new ToolValidationError(`Unknown tool: ${toolName}`);
|
|
191
|
+
}
|
|
192
|
+
validateArgs(tool, args);
|
|
193
|
+
return executor(tool.buildRequest(args));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/index.ts
|
|
197
|
+
var API_KEY = process.env.METRONE_API_KEY;
|
|
198
|
+
var ENDPOINT = process.env.METRONE_ENDPOINT ?? "https://api.metrone.io";
|
|
199
|
+
if (!API_KEY) {
|
|
200
|
+
console.error("METRONE_API_KEY environment variable is required");
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
204
|
+
var httpExecutor = async (req) => {
|
|
205
|
+
const url = new URL(req.path, ENDPOINT);
|
|
206
|
+
const controller = new AbortController();
|
|
207
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
208
|
+
try {
|
|
209
|
+
let res;
|
|
210
|
+
if (req.method === "GET") {
|
|
211
|
+
for (const [k, v] of Object.entries(req.query)) {
|
|
212
|
+
if (v !== void 0 && v !== "") url.searchParams.set(k, v);
|
|
213
|
+
}
|
|
214
|
+
res = await fetch(url.toString(), {
|
|
215
|
+
headers: { "X-Api-Key": API_KEY },
|
|
216
|
+
signal: controller.signal
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
res = await fetch(url.toString(), {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
|
|
222
|
+
body: JSON.stringify({ api_key: API_KEY, ...req.body }),
|
|
223
|
+
signal: controller.signal
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
const body = await res.text();
|
|
228
|
+
throw new Error(`Metrone API error ${res.status}: ${body}`);
|
|
229
|
+
}
|
|
230
|
+
return res.json();
|
|
231
|
+
} finally {
|
|
232
|
+
clearTimeout(timer);
|
|
150
233
|
}
|
|
234
|
+
};
|
|
235
|
+
var server = new Server(
|
|
236
|
+
{ name: "metrone", version: "1.0.1" },
|
|
237
|
+
{ capabilities: { tools: {} } }
|
|
151
238
|
);
|
|
239
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
240
|
+
tools: listTools()
|
|
241
|
+
}));
|
|
242
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
243
|
+
const { name, arguments: args } = request.params;
|
|
244
|
+
try {
|
|
245
|
+
const data = await dispatch(name, args ?? {}, httpExecutor);
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
|
|
252
|
+
isError: true
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
});
|
|
152
256
|
var transport = new StdioServerTransport();
|
|
153
257
|
await server.connect(transport);
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.ts — shared MCP tool registry (v1.1 SPEC-06A)
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for Metrone's MCP tools, consumed by BOTH transports:
|
|
5
|
+
*
|
|
6
|
+
* - stdio (src/index.ts, the published @metrone-io/mcp binary): injects an
|
|
7
|
+
* HTTP executor that fetches METRONE_ENDPOINT with the env API key.
|
|
8
|
+
* - HTTP (packages/worker /mcp route): injects an INTERNAL executor that
|
|
9
|
+
* synthesizes Requests against the worker's own handlers — no self-fetch
|
|
10
|
+
* (Cloudflare blocks same-zone worker self-fetch) and no extra network hop.
|
|
11
|
+
*
|
|
12
|
+
* Adding a tool here makes it available on both transports automatically
|
|
13
|
+
* (this is the contract SPEC-06B relies on when it adds 8 more tools).
|
|
14
|
+
*
|
|
15
|
+
* Design constraints:
|
|
16
|
+
* - This module is dependency-free (no zod, no MCP SDK) so the worker can
|
|
17
|
+
* bundle it without dragging Node-targeted dependencies into the
|
|
18
|
+
* Cloudflare runtime. Input schemas are plain JSON Schema — which is
|
|
19
|
+
* what the MCP wire protocol (tools/list) wants anyway.
|
|
20
|
+
* - Validation here is intentionally light (required fields + basic types).
|
|
21
|
+
* The REST API performs the authoritative validation; duplicating it
|
|
22
|
+
* would drift.
|
|
23
|
+
*/
|
|
24
|
+
/** A request a tool wants to make against the Metrone REST API. */
|
|
25
|
+
type ToolApiRequest = {
|
|
26
|
+
method: 'GET';
|
|
27
|
+
path: string;
|
|
28
|
+
query: Record<string, string>;
|
|
29
|
+
} | {
|
|
30
|
+
method: 'POST';
|
|
31
|
+
path: string;
|
|
32
|
+
body: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Transport-specific executor. Receives the API request spec, performs it
|
|
36
|
+
* (HTTP fetch for stdio, internal handler dispatch for the worker), and
|
|
37
|
+
* returns the parsed JSON response body. MUST throw on non-2xx responses —
|
|
38
|
+
* dispatch() converts throws into MCP isError tool results.
|
|
39
|
+
*/
|
|
40
|
+
type ToolExecutor = (req: ToolApiRequest) => Promise<unknown>;
|
|
41
|
+
type ToolScope = 'read' | 'write';
|
|
42
|
+
interface ToolDescriptor {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
/** Plain JSON Schema (draft-07 subset) for the MCP tools/list response. */
|
|
46
|
+
inputSchema: Record<string, unknown>;
|
|
47
|
+
/** Minimum API-key scope this tool needs. */
|
|
48
|
+
scope: ToolScope;
|
|
49
|
+
/** Translate validated args into an API request spec. */
|
|
50
|
+
buildRequest: (args: Record<string, unknown>) => ToolApiRequest;
|
|
51
|
+
}
|
|
52
|
+
declare const TOOLS: ToolDescriptor[];
|
|
53
|
+
/** Tool list in MCP tools/list wire format. */
|
|
54
|
+
declare function listTools(): Array<{
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
inputSchema: Record<string, unknown>;
|
|
58
|
+
}>;
|
|
59
|
+
declare function getTool(name: string): ToolDescriptor | undefined;
|
|
60
|
+
declare class ToolValidationError extends Error {
|
|
61
|
+
constructor(message: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate args against the tool's required fields. Light by design — the
|
|
65
|
+
* REST API is the authoritative validator. Throws ToolValidationError so
|
|
66
|
+
* transports can map it to the right error shape (-32602 for JSON-RPC,
|
|
67
|
+
* isError result for tool calls).
|
|
68
|
+
*/
|
|
69
|
+
declare function validateArgs(tool: ToolDescriptor, args: Record<string, unknown>): void;
|
|
70
|
+
/**
|
|
71
|
+
* Execute a tool by name through the injected executor. Returns the parsed
|
|
72
|
+
* API response. Throws:
|
|
73
|
+
* - ToolValidationError for unknown tool / missing required args
|
|
74
|
+
* - whatever the executor throws for API failures
|
|
75
|
+
*/
|
|
76
|
+
declare function dispatch(toolName: string, args: Record<string, unknown>, executor: ToolExecutor): Promise<unknown>;
|
|
77
|
+
|
|
78
|
+
export { TOOLS, type ToolApiRequest, type ToolDescriptor, type ToolExecutor, type ToolScope, ToolValidationError, dispatch, getTool, listTools, validateArgs };
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/tools.ts
|
|
2
|
+
function num(v) {
|
|
3
|
+
return typeof v === "number" && Number.isFinite(v) ? String(v) : void 0;
|
|
4
|
+
}
|
|
5
|
+
function str(v) {
|
|
6
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7
|
+
}
|
|
8
|
+
function query(entries) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
11
|
+
if (v !== void 0) out[k] = v;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
var TOOLS = [
|
|
16
|
+
{
|
|
17
|
+
name: "metrone_get_stats",
|
|
18
|
+
description: "Get aggregated analytics statistics (events, pageviews, visitors, sessions, conversion rate) for a time period.",
|
|
19
|
+
scope: "read",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
days: { type: "number", description: "Look-back window in days (default 30)" },
|
|
24
|
+
from: { type: "string", description: "ISO 8601 start date (overrides days)" },
|
|
25
|
+
to: { type: "string", description: "ISO 8601 end date" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
buildRequest: (args) => ({
|
|
29
|
+
method: "GET",
|
|
30
|
+
path: "/v1/api/stats",
|
|
31
|
+
query: query({ days: num(args.days), from: str(args.from), to: str(args.to) })
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "metrone_get_events",
|
|
36
|
+
description: "Query raw analytics events with optional filters for event type and source.",
|
|
37
|
+
scope: "read",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
days: { type: "number" },
|
|
42
|
+
limit: { type: "number" },
|
|
43
|
+
event_type: { type: "string" },
|
|
44
|
+
source: { type: "string" }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
buildRequest: (args) => ({
|
|
48
|
+
method: "GET",
|
|
49
|
+
path: "/v1/api/events",
|
|
50
|
+
query: query({
|
|
51
|
+
days: num(args.days),
|
|
52
|
+
limit: num(args.limit),
|
|
53
|
+
event_type: str(args.event_type),
|
|
54
|
+
source: str(args.source)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "metrone_get_pages",
|
|
60
|
+
description: "Get top pages by views and sessions.",
|
|
61
|
+
scope: "read",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
days: { type: "number" },
|
|
66
|
+
limit: { type: "number" }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
buildRequest: (args) => ({
|
|
70
|
+
method: "GET",
|
|
71
|
+
path: "/v1/api/pages",
|
|
72
|
+
query: query({ days: num(args.days), limit: num(args.limit) })
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "metrone_get_sources",
|
|
77
|
+
description: "Get traffic source and referrer breakdown.",
|
|
78
|
+
scope: "read",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
days: { type: "number" }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
buildRequest: (args) => ({
|
|
86
|
+
method: "GET",
|
|
87
|
+
path: "/v1/api/sources",
|
|
88
|
+
query: query({ days: num(args.days) })
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "metrone_get_live",
|
|
93
|
+
description: "Get real-time stats: active visitors and today\u2019s totals.",
|
|
94
|
+
scope: "read",
|
|
95
|
+
inputSchema: { type: "object", properties: {} },
|
|
96
|
+
buildRequest: () => ({ method: "GET", path: "/v1/api/live", query: {} })
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "metrone_track_event",
|
|
100
|
+
description: "Track a custom analytics event.",
|
|
101
|
+
scope: "write",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
event_type: { type: "string" },
|
|
106
|
+
source: { type: "string", description: 'Event source (defaults to "assistant")' },
|
|
107
|
+
event_name: { type: "string" },
|
|
108
|
+
page_url: { type: "string" },
|
|
109
|
+
properties: { type: "object" }
|
|
110
|
+
},
|
|
111
|
+
required: ["event_type"]
|
|
112
|
+
},
|
|
113
|
+
buildRequest: (args) => {
|
|
114
|
+
const body = {
|
|
115
|
+
event_type: args.event_type,
|
|
116
|
+
source: str(args.source) ?? "assistant"
|
|
117
|
+
};
|
|
118
|
+
if (str(args.event_name)) body.event_name = args.event_name;
|
|
119
|
+
if (str(args.page_url)) body.page_url = args.page_url;
|
|
120
|
+
if (args.properties && typeof args.properties === "object") body.properties = args.properties;
|
|
121
|
+
return { method: "POST", path: "/v1/api/events", body };
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "metrone_track_ai_call",
|
|
126
|
+
description: "Track an AI call event (voice / chat / assistant interaction).",
|
|
127
|
+
scope: "write",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
call_id: { type: "string" },
|
|
132
|
+
provider: { type: "string" },
|
|
133
|
+
source: { type: "string", enum: ["voice", "chat", "assistant"] },
|
|
134
|
+
duration: { type: "number", description: "Duration in seconds" },
|
|
135
|
+
intent: { type: "string" },
|
|
136
|
+
outcome: { type: "string" },
|
|
137
|
+
properties: { type: "object" }
|
|
138
|
+
},
|
|
139
|
+
required: ["call_id", "provider"]
|
|
140
|
+
},
|
|
141
|
+
buildRequest: (args) => {
|
|
142
|
+
const body = {
|
|
143
|
+
event_type: "ai_call",
|
|
144
|
+
source: str(args.source) ?? "voice",
|
|
145
|
+
ai_call_id: args.call_id,
|
|
146
|
+
ai_provider: args.provider
|
|
147
|
+
};
|
|
148
|
+
if (typeof args.duration === "number") body.ai_duration_sec = args.duration;
|
|
149
|
+
if (str(args.intent)) body.ai_intent = args.intent;
|
|
150
|
+
const props = args.properties && typeof args.properties === "object" ? args.properties : void 0;
|
|
151
|
+
if (props || str(args.outcome)) {
|
|
152
|
+
body.properties = { ...props, ...str(args.outcome) ? { outcome: args.outcome } : {} };
|
|
153
|
+
}
|
|
154
|
+
return { method: "POST", path: "/v1/api/events", body };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
var TOOLS_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
159
|
+
function listTools() {
|
|
160
|
+
return TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
|
|
161
|
+
}
|
|
162
|
+
function getTool(name) {
|
|
163
|
+
return TOOLS_BY_NAME.get(name);
|
|
164
|
+
}
|
|
165
|
+
var ToolValidationError = class extends Error {
|
|
166
|
+
constructor(message) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = "ToolValidationError";
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function validateArgs(tool, args) {
|
|
172
|
+
const required = tool.inputSchema.required ?? [];
|
|
173
|
+
for (const field of required) {
|
|
174
|
+
const v = args[field];
|
|
175
|
+
if (v === void 0 || v === null || typeof v === "string" && v.length === 0) {
|
|
176
|
+
throw new ToolValidationError(`Missing required argument: ${field}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function dispatch(toolName, args, executor) {
|
|
181
|
+
const tool = getTool(toolName);
|
|
182
|
+
if (!tool) {
|
|
183
|
+
throw new ToolValidationError(`Unknown tool: ${toolName}`);
|
|
184
|
+
}
|
|
185
|
+
validateArgs(tool, args);
|
|
186
|
+
return executor(tool.buildRequest(args));
|
|
187
|
+
}
|
|
188
|
+
export {
|
|
189
|
+
TOOLS,
|
|
190
|
+
ToolValidationError,
|
|
191
|
+
dispatch,
|
|
192
|
+
getTool,
|
|
193
|
+
listTools,
|
|
194
|
+
validateArgs
|
|
195
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metrone-io/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Metrone MCP server — expose analytics to AI agents via Model Context Protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./tools": {
|
|
10
|
+
"types": "./dist/tools.d.ts",
|
|
11
|
+
"default": "./dist/tools.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
7
14
|
"bin": {
|
|
8
15
|
"metrone-mcp": "./dist/index.js"
|
|
9
16
|
},
|
|
@@ -12,6 +19,7 @@
|
|
|
12
19
|
"dev": "tsup --watch",
|
|
13
20
|
"start": "node dist/index.js",
|
|
14
21
|
"clean": "rimraf dist",
|
|
22
|
+
"prepare": "npm run build",
|
|
15
23
|
"prepublishOnly": "npm run clean && npm run build"
|
|
16
24
|
},
|
|
17
25
|
"publishConfig": {
|
|
@@ -21,9 +29,27 @@
|
|
|
21
29
|
"dist",
|
|
22
30
|
"README.md"
|
|
23
31
|
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"metrone",
|
|
34
|
+
"mcp",
|
|
35
|
+
"model-context-protocol",
|
|
36
|
+
"analytics",
|
|
37
|
+
"ai-agents"
|
|
38
|
+
],
|
|
39
|
+
"author": "Metrone",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/metrone-io/sdks.git",
|
|
44
|
+
"directory": "packages/mcp"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/metrone-io/sdks/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://metrone.io",
|
|
24
50
|
"dependencies": {
|
|
25
51
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
26
|
-
"zod": "^
|
|
52
|
+
"zod": "^3.23.0"
|
|
27
53
|
},
|
|
28
54
|
"devDependencies": {
|
|
29
55
|
"tsup": "^8.0.0",
|