@probebrowser/mcp-server 1.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/Dockerfile +59 -0
- package/dist/index.js +29 -0
- package/dist/resources.js +1 -0
- package/dist/server.js +282 -0
- package/dist/tools.js +761 -0
- package/package.json +46 -0
- package/src/index.ts +34 -0
- package/src/resources.ts +0 -0
- package/src/server.ts +330 -0
- package/src/tools.ts +774 -0
- package/tsconfig.json +16 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
FROM node:20-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install build dependencies if needed (e.g. for native modules)
|
|
6
|
+
# RUN apt-get update && apt-get install -y python3 make g++
|
|
7
|
+
|
|
8
|
+
# Copy workspace configuration
|
|
9
|
+
COPY package.json .
|
|
10
|
+
|
|
11
|
+
# Copy package manifests to install dependencies first (caching)
|
|
12
|
+
COPY packages/sdk/package.json packages/sdk/
|
|
13
|
+
COPY packages/mcp-server/package.json packages/mcp-server/
|
|
14
|
+
|
|
15
|
+
# Install SDK dependencies
|
|
16
|
+
WORKDIR /app/packages/sdk
|
|
17
|
+
RUN npm install
|
|
18
|
+
|
|
19
|
+
# Install MCP Server dependencies
|
|
20
|
+
WORKDIR /app/packages/mcp-server
|
|
21
|
+
RUN npm install
|
|
22
|
+
|
|
23
|
+
# Copy source code
|
|
24
|
+
COPY packages/sdk /app/packages/sdk
|
|
25
|
+
COPY packages/mcp-server /app/packages/mcp-server
|
|
26
|
+
|
|
27
|
+
# Build SDK
|
|
28
|
+
WORKDIR /app/packages/sdk
|
|
29
|
+
RUN npm run build
|
|
30
|
+
|
|
31
|
+
# Build MCP Server
|
|
32
|
+
WORKDIR /app/packages/mcp-server
|
|
33
|
+
# Ensure local SDK is used
|
|
34
|
+
RUN npm install ../sdk
|
|
35
|
+
RUN npm run build
|
|
36
|
+
|
|
37
|
+
# Cleanup dev dependencies (optional optimization)
|
|
38
|
+
# RUN npm prune --production
|
|
39
|
+
|
|
40
|
+
# Runtime Configuration
|
|
41
|
+
ENV NODE_ENV=production
|
|
42
|
+
ENV TRANSPORT=sse
|
|
43
|
+
ENV PORT=3000
|
|
44
|
+
# IMPORTANT: Trace needs a browser.
|
|
45
|
+
# In Docker, you typically connect to a remote browser (browserless.io)
|
|
46
|
+
# or install Chromium. For this base image, we assume remote connect
|
|
47
|
+
# or headless with Puppeteer installing Chrome (which needs libs).
|
|
48
|
+
# Installing Chrome dependencies:
|
|
49
|
+
RUN apt-get update && apt-get install -y wget gnupg \
|
|
50
|
+
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
|
51
|
+
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
|
52
|
+
&& apt-get update \
|
|
53
|
+
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
|
54
|
+
--no-install-recommends \
|
|
55
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
56
|
+
|
|
57
|
+
EXPOSE 3000
|
|
58
|
+
|
|
59
|
+
CMD ["node", "dist/index.js"]
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
4
|
+
import { TraceMcpServer } from './server.js';
|
|
5
|
+
async function main() {
|
|
6
|
+
const transportType = process.env.TRANSPORT || 'stdio';
|
|
7
|
+
if (transportType === 'sse') {
|
|
8
|
+
const app = express();
|
|
9
|
+
app.use(cors());
|
|
10
|
+
// Create MCP Server
|
|
11
|
+
const traceServer = new TraceMcpServer();
|
|
12
|
+
const transport = new SSEServerTransport('/messages', app); // Type cast might be needed if SDK types mismatch express
|
|
13
|
+
await traceServer.connect(transport);
|
|
14
|
+
const port = process.env.PORT || 3000;
|
|
15
|
+
app.listen(port, () => {
|
|
16
|
+
console.error(`Trace MCP Server (SSE) running on port ${port}`);
|
|
17
|
+
console.error(`Endpoint: http://localhost:${port}/sse`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Stdio Mode
|
|
22
|
+
const traceServer = new TraceMcpServer();
|
|
23
|
+
await traceServer.start();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
main().catch((error) => {
|
|
27
|
+
console.error('Fatal error:', error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { Trace, ALL_TOOLS } from '@probebrowser/sdk';
|
|
5
|
+
import { TOOLS } from './tools.js';
|
|
6
|
+
// Schema conversion helper
|
|
7
|
+
function zodToJsonSchema(schema) {
|
|
8
|
+
return {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {}, // Simplified
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export class TraceMcpServer {
|
|
14
|
+
server;
|
|
15
|
+
trace;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.server = new Server({
|
|
18
|
+
name: 'trace-mcp-server',
|
|
19
|
+
version: '1.2.0',
|
|
20
|
+
}, {
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {},
|
|
23
|
+
resources: {},
|
|
24
|
+
prompts: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// Initialize Trace SDK with Env Config
|
|
28
|
+
const headless = process.env.TRACE_HEADLESS !== 'false'; // Default to true
|
|
29
|
+
const verbose = process.env.TRACE_VERBOSE === 'true';
|
|
30
|
+
this.trace = new Trace({
|
|
31
|
+
headless,
|
|
32
|
+
verbose,
|
|
33
|
+
});
|
|
34
|
+
this.setupHandlers();
|
|
35
|
+
}
|
|
36
|
+
setupHandlers() {
|
|
37
|
+
// ============================================
|
|
38
|
+
// 1. TOOLS
|
|
39
|
+
// ============================================
|
|
40
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
41
|
+
const definedTools = Object.entries(TOOLS).map(([name, def]) => ({
|
|
42
|
+
name,
|
|
43
|
+
description: def.description,
|
|
44
|
+
inputSchema: zodToJsonSchema(def.schema),
|
|
45
|
+
}));
|
|
46
|
+
const definedNames = new Set(Object.keys(TOOLS));
|
|
47
|
+
const distinctAllTools = new Set(ALL_TOOLS);
|
|
48
|
+
// Add dynamic tools
|
|
49
|
+
for (const tool of distinctAllTools) {
|
|
50
|
+
const mcpName = `trace_${tool}`;
|
|
51
|
+
if (!definedNames.has(mcpName)) {
|
|
52
|
+
definedTools.push({
|
|
53
|
+
name: mcpName,
|
|
54
|
+
description: `Execute ${tool} (Dynamic Tool)`,
|
|
55
|
+
inputSchema: { type: 'object', properties: {} },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { tools: definedTools };
|
|
60
|
+
});
|
|
61
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
62
|
+
const toolName = request.params.name;
|
|
63
|
+
let toolDef = TOOLS[toolName];
|
|
64
|
+
let method = toolDef?.method;
|
|
65
|
+
// Dynamic Tool Logic
|
|
66
|
+
if (!toolDef && toolName.startsWith('trace_')) {
|
|
67
|
+
const rawName = toolName.replace('trace_', '');
|
|
68
|
+
if (ALL_TOOLS.includes(rawName)) {
|
|
69
|
+
method = rawName;
|
|
70
|
+
toolDef = { method: rawName };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!method)
|
|
74
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
75
|
+
const isConnected = !!this.trace.getCurrentUrl();
|
|
76
|
+
try {
|
|
77
|
+
let result;
|
|
78
|
+
// 1. Handle Connect Tool
|
|
79
|
+
if (toolDef.method === 'connect') {
|
|
80
|
+
const url = String(request.params.arguments?.url);
|
|
81
|
+
// Re-init trace if headless mode changes? For now just connect.
|
|
82
|
+
await this.trace.connect(url);
|
|
83
|
+
return { content: [{ type: 'text', text: `Connected to ${url}` }] };
|
|
84
|
+
}
|
|
85
|
+
if (!isConnected) {
|
|
86
|
+
return { content: [{ type: 'text', text: 'Error: No active page. Call trace_connect first.' }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
// 2. Handle Azure AI Agent (Unified Deep Debug)
|
|
89
|
+
if (toolDef.method === 'deep_debug' || toolDef.method === 'trace_p') {
|
|
90
|
+
// Check for API Key
|
|
91
|
+
if (!process.env.TRACE_API_KEY) {
|
|
92
|
+
return { content: [{ type: 'text', text: 'Error: TRACE_API_KEY is required for AI features.' }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
const prompt = String(request.params.arguments?.prompt || request.params.arguments?.q || 'Analyze page');
|
|
95
|
+
// Delegate to SDK's queryDeep which calls Azure /v1/agent-step
|
|
96
|
+
const analysis = await this.trace.queryDeep(prompt);
|
|
97
|
+
// Return the conclusion and steps
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{ type: 'text', text: `### Conclusion\n${analysis.conclusion}\n\n### Steps Taken\n${analysis.steps.map(s => `- ${s.tool}: ${s.success ? 'Success' : 'Failed'}`).join('\n')}` }
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// 3. Handle Standard SDK Tools
|
|
105
|
+
result = await this.trace.executeTool(toolDef.method, request.params.arguments);
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// ============================================
|
|
118
|
+
// 2. RESOURCES
|
|
119
|
+
// ============================================
|
|
120
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
121
|
+
return {
|
|
122
|
+
resources: [
|
|
123
|
+
{
|
|
124
|
+
uri: 'trace://console/errors',
|
|
125
|
+
name: 'Console Errors',
|
|
126
|
+
mimeType: 'application/json',
|
|
127
|
+
description: 'List of all console errors and exceptions',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
uri: 'trace://console/logs',
|
|
131
|
+
name: 'All Console Logs',
|
|
132
|
+
mimeType: 'application/json',
|
|
133
|
+
description: 'Full console history (info, warn, error)',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
uri: 'trace://network/failed',
|
|
137
|
+
name: 'Failed Network Requests',
|
|
138
|
+
mimeType: 'application/json',
|
|
139
|
+
description: 'List of failed network requests (4xx, 5xx)',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
uri: 'trace://network/all',
|
|
143
|
+
name: 'All Network Traffic',
|
|
144
|
+
mimeType: 'application/json',
|
|
145
|
+
description: 'Recent network requests summary',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
uri: 'trace://dom/tree',
|
|
149
|
+
name: 'DOM Tree',
|
|
150
|
+
mimeType: 'application/json',
|
|
151
|
+
description: 'Simplified DOM structure',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
uri: 'trace://screenshot',
|
|
155
|
+
name: 'Page Screenshot',
|
|
156
|
+
mimeType: 'image/png',
|
|
157
|
+
description: 'Current page screenshot',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
163
|
+
const uri = request.params.uri;
|
|
164
|
+
if (!this.trace.getCurrentUrl()) {
|
|
165
|
+
throw new Error('Not connected. Call trace_connect first.');
|
|
166
|
+
}
|
|
167
|
+
// --- Console ---
|
|
168
|
+
if (uri === 'trace://console/errors') {
|
|
169
|
+
const logs = await this.trace.executeTool('get_console_errors', {});
|
|
170
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(logs, null, 2) }] };
|
|
171
|
+
}
|
|
172
|
+
if (uri === 'trace://console/logs') {
|
|
173
|
+
// We don't have get_console_logs explicitly in ALL_TOOLS list in previous step,
|
|
174
|
+
// but we can use get_console_summary or implement a getter.
|
|
175
|
+
// For now, let's return the summary as it contains counts.
|
|
176
|
+
// Ideally SDK should expose 'get_all_logs'.
|
|
177
|
+
// Fallback: use get_console_errors
|
|
178
|
+
const logs = await this.trace.executeTool('get_console_summary', {});
|
|
179
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(logs, null, 2) }] };
|
|
180
|
+
}
|
|
181
|
+
// --- Network ---
|
|
182
|
+
if (uri === 'trace://network/failed') {
|
|
183
|
+
const reqs = await this.trace.executeTool('get_network_failed', {});
|
|
184
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(reqs, null, 2) }] };
|
|
185
|
+
}
|
|
186
|
+
if (uri === 'trace://network/all') {
|
|
187
|
+
const reqs = await this.trace.executeTool('get_network_summary', {});
|
|
188
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(reqs, null, 2) }] };
|
|
189
|
+
}
|
|
190
|
+
// --- DOM ---
|
|
191
|
+
if (uri === 'trace://dom/tree') {
|
|
192
|
+
const tree = await this.trace.executeTool('get_dom_tree', { depth: 2 });
|
|
193
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(tree, null, 2) }] };
|
|
194
|
+
}
|
|
195
|
+
// --- Screenshot ---
|
|
196
|
+
if (uri === 'trace://screenshot') {
|
|
197
|
+
// Try to capture snapshot via capture_execution_state which might have screenshot
|
|
198
|
+
// Or just return a placeholder if SDK doesn't support raw image output via tool interface easily
|
|
199
|
+
// For now, returning text explainer
|
|
200
|
+
return { contents: [{ uri, mimeType: 'text/plain', text: "Screenshot resource requires Trace SDK v1.4+" }] };
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
203
|
+
});
|
|
204
|
+
// ============================================
|
|
205
|
+
// 3. PROMPTS
|
|
206
|
+
// ============================================
|
|
207
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
208
|
+
return {
|
|
209
|
+
prompts: [
|
|
210
|
+
{
|
|
211
|
+
name: 'debug_error',
|
|
212
|
+
description: 'Analyze the most recent error and suggest a fix',
|
|
213
|
+
arguments: [],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'audit_performance',
|
|
217
|
+
description: 'Check metrics and run a quick profile',
|
|
218
|
+
arguments: [],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'verify_security',
|
|
222
|
+
description: 'Check security headers and SSL status',
|
|
223
|
+
arguments: [],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
229
|
+
const promptName = request.params.name;
|
|
230
|
+
if (promptName === 'debug_error') {
|
|
231
|
+
return {
|
|
232
|
+
messages: [
|
|
233
|
+
{
|
|
234
|
+
role: 'user',
|
|
235
|
+
content: {
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: 'Please check the console for errors, analyze the most critical one, and suggest a code fix.',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (promptName === 'audit_performance') {
|
|
244
|
+
return {
|
|
245
|
+
messages: [
|
|
246
|
+
{
|
|
247
|
+
role: 'user',
|
|
248
|
+
content: {
|
|
249
|
+
type: 'text',
|
|
250
|
+
text: 'Please capture performance metrics, take a heap snapshot, and tell me if there are any obvious bottlenecks.',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (promptName === 'verify_security') {
|
|
257
|
+
return {
|
|
258
|
+
messages: [
|
|
259
|
+
{
|
|
260
|
+
role: 'user',
|
|
261
|
+
content: {
|
|
262
|
+
type: 'text',
|
|
263
|
+
text: 'Please check the security info, verify HTTPS, and look for mixed content warnings.',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
throw new Error('Prompt not found');
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async connect(transport) {
|
|
273
|
+
await this.server.connect(transport);
|
|
274
|
+
console.error('Trace MCP Server connected');
|
|
275
|
+
}
|
|
276
|
+
async start() {
|
|
277
|
+
// Default to Stdio for backward compatibility
|
|
278
|
+
const transport = new StdioServerTransport();
|
|
279
|
+
await this.server.connect(transport);
|
|
280
|
+
console.error('Trace MCP Server running on stdio');
|
|
281
|
+
}
|
|
282
|
+
}
|