mirror of
https://github.com/AgentDeskAI/browser-tools-mcp.git
synced 2025-11-14 09:08:02 +00:00
first commit
This commit is contained in:
commit
d5ed2ef2d7
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.port
|
||||
11
chrome-extension/devtools.html
Normal file
11
chrome-extension/devtools.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>DevTools Logs</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- DevTools extension script -->
|
||||
<script src="devtools.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
384
chrome-extension/devtools.js
Normal file
384
chrome-extension/devtools.js
Normal file
@ -0,0 +1,384 @@
|
||||
// devtools.js
|
||||
|
||||
// Store settings with defaults
|
||||
let settings = {
|
||||
logLimit: 50,
|
||||
queryLimit: 30000,
|
||||
stringSizeLimit: 500,
|
||||
maxLogSize: 20000,
|
||||
showRequestHeaders: false,
|
||||
showResponseHeaders: false,
|
||||
};
|
||||
|
||||
// Keep track of debugger state
|
||||
let isDebuggerAttached = false;
|
||||
const currentTabId = chrome.devtools.inspectedWindow.tabId;
|
||||
|
||||
// Load saved settings on startup
|
||||
chrome.storage.local.get(["browserConnectorSettings"], (result) => {
|
||||
if (result.browserConnectorSettings) {
|
||||
settings = { ...settings, ...result.browserConnectorSettings };
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for settings updates
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "SETTINGS_UPDATED") {
|
||||
settings = message.settings;
|
||||
}
|
||||
});
|
||||
|
||||
// Utility to recursively truncate strings in any data structure
|
||||
function truncateStringsInData(data, maxLength, depth = 0, path = "") {
|
||||
// Add depth limit to prevent circular references
|
||||
if (depth > 100) {
|
||||
console.warn("Max depth exceeded at path:", path);
|
||||
return "[MAX_DEPTH_EXCEEDED]";
|
||||
}
|
||||
|
||||
console.log(`Processing at path: ${path}, type:`, typeof data);
|
||||
|
||||
if (typeof data === "string") {
|
||||
if (data.length > maxLength) {
|
||||
console.log(
|
||||
`Truncating string at path ${path} from ${data.length} to ${maxLength}`
|
||||
);
|
||||
return data.substring(0, maxLength) + "... (truncated)";
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
console.log(`Processing array at path ${path} with length:`, data.length);
|
||||
return data.map((item, index) =>
|
||||
truncateStringsInData(item, maxLength, depth + 1, `${path}[${index}]`)
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof data === "object" && data !== null) {
|
||||
console.log(
|
||||
`Processing object at path ${path} with keys:`,
|
||||
Object.keys(data)
|
||||
);
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
try {
|
||||
result[key] = truncateStringsInData(
|
||||
value,
|
||||
maxLength,
|
||||
depth + 1,
|
||||
path ? `${path}.${key}` : key
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`Error processing key ${key} at path ${path}:`, e);
|
||||
result[key] = "[ERROR_PROCESSING]";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Helper to calculate the size of an object
|
||||
function calculateObjectSize(obj) {
|
||||
return JSON.stringify(obj).length;
|
||||
}
|
||||
|
||||
// Helper to process array of objects with size limit
|
||||
function processArrayWithSizeLimit(array, maxTotalSize, processFunc) {
|
||||
let currentSize = 0;
|
||||
const result = [];
|
||||
|
||||
for (const item of array) {
|
||||
// Process the item first
|
||||
const processedItem = processFunc(item);
|
||||
const itemSize = calculateObjectSize(processedItem);
|
||||
|
||||
// Check if adding this item would exceed the limit
|
||||
if (currentSize + itemSize > maxTotalSize) {
|
||||
console.log(
|
||||
`Reached size limit (${currentSize}/${maxTotalSize}), truncating array`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add item and update size
|
||||
result.push(processedItem);
|
||||
currentSize += itemSize;
|
||||
console.log(
|
||||
`Added item of size ${itemSize}, total size now: ${currentSize}`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Modified processJsonString to handle arrays with size limit
|
||||
function processJsonString(jsonString, maxLength) {
|
||||
console.log("Processing string of length:", jsonString?.length);
|
||||
try {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(jsonString);
|
||||
console.log(
|
||||
"Successfully parsed as JSON, structure:",
|
||||
JSON.stringify(Object.keys(parsed))
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Not valid JSON, treating as string");
|
||||
return truncateStringsInData(jsonString, maxLength, 0, "root");
|
||||
}
|
||||
|
||||
// If it's an array, process with size limit
|
||||
if (Array.isArray(parsed)) {
|
||||
console.log("Processing array of objects with size limit");
|
||||
const processed = processArrayWithSizeLimit(
|
||||
parsed,
|
||||
settings.maxLogSize,
|
||||
(item) => truncateStringsInData(item, maxLength, 0, "root")
|
||||
);
|
||||
const result = JSON.stringify(processed);
|
||||
console.log(
|
||||
`Processed array: ${parsed.length} -> ${processed.length} items`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Otherwise process as before
|
||||
const processed = truncateStringsInData(parsed, maxLength, 0, "root");
|
||||
const result = JSON.stringify(processed);
|
||||
console.log("Processed JSON string length:", result.length);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error("Error in processJsonString:", e);
|
||||
return jsonString.substring(0, maxLength) + "... (truncated)";
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to send logs to browser-connector
|
||||
function sendToBrowserConnector(logData) {
|
||||
console.log("Original log data size:", JSON.stringify(logData).length);
|
||||
|
||||
// Process any string fields that might contain JSON
|
||||
const processedData = { ...logData };
|
||||
|
||||
if (logData.type === "network-request") {
|
||||
console.log("Processing network request");
|
||||
if (processedData.requestBody) {
|
||||
console.log(
|
||||
"Request body size before:",
|
||||
processedData.requestBody.length
|
||||
);
|
||||
processedData.requestBody = processJsonString(
|
||||
processedData.requestBody,
|
||||
settings.stringSizeLimit
|
||||
);
|
||||
console.log("Request body size after:", processedData.requestBody.length);
|
||||
}
|
||||
if (processedData.responseBody) {
|
||||
console.log(
|
||||
"Response body size before:",
|
||||
processedData.responseBody.length
|
||||
);
|
||||
processedData.responseBody = processJsonString(
|
||||
processedData.responseBody,
|
||||
settings.stringSizeLimit
|
||||
);
|
||||
console.log(
|
||||
"Response body size after:",
|
||||
processedData.responseBody.length
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
logData.type === "console-log" ||
|
||||
logData.type === "console-error"
|
||||
) {
|
||||
console.log("Processing console message");
|
||||
if (processedData.message) {
|
||||
console.log("Message size before:", processedData.message.length);
|
||||
processedData.message = processJsonString(
|
||||
processedData.message,
|
||||
settings.stringSizeLimit
|
||||
);
|
||||
console.log("Message size after:", processedData.message.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Add settings to the request
|
||||
const payload = {
|
||||
data: {
|
||||
...processedData,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
settings: {
|
||||
logLimit: settings.logLimit,
|
||||
queryLimit: settings.queryLimit,
|
||||
showRequestHeaders: settings.showRequestHeaders,
|
||||
showResponseHeaders: settings.showResponseHeaders,
|
||||
},
|
||||
};
|
||||
|
||||
const finalPayloadSize = JSON.stringify(payload).length;
|
||||
console.log("Final payload size:", finalPayloadSize);
|
||||
|
||||
if (finalPayloadSize > 1000000) {
|
||||
// 1MB warning threshold
|
||||
console.warn("Warning: Large payload detected:", finalPayloadSize);
|
||||
console.warn(
|
||||
"Payload preview:",
|
||||
JSON.stringify(payload).substring(0, 1000) + "..."
|
||||
);
|
||||
}
|
||||
|
||||
fetch("http://127.0.0.1:3025/extension-log", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
}).catch((error) => {
|
||||
console.error("Failed to send log to browser-connector:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Listen for network requests
|
||||
chrome.devtools.network.onRequestFinished.addListener((request) => {
|
||||
if (request._resourceType === "xhr" || request._resourceType === "fetch") {
|
||||
request.getContent((responseBody) => {
|
||||
const entry = {
|
||||
type: "network-request",
|
||||
url: request.request.url,
|
||||
method: request.request.method,
|
||||
status: request.response.status,
|
||||
requestHeaders: request.request.headers,
|
||||
responseHeaders: request.response.headers,
|
||||
requestBody: request.request.postData?.text ?? "",
|
||||
responseBody: responseBody ?? "",
|
||||
};
|
||||
sendToBrowserConnector(entry);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Move the console message listener outside the panel creation
|
||||
const consoleMessageListener = (source, method, params) => {
|
||||
console.log("Debugger event:", method, source.tabId, currentTabId);
|
||||
|
||||
// Only process events for our tab
|
||||
if (source.tabId !== currentTabId) {
|
||||
console.log("Ignoring event from different tab");
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "Console.messageAdded") {
|
||||
console.log("Console message received:", params.message);
|
||||
const entry = {
|
||||
type: params.message.level === "error" ? "console-error" : "console-log",
|
||||
level: params.message.level,
|
||||
message: params.message.text,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
console.log("Sending console entry:", entry);
|
||||
sendToBrowserConnector(entry);
|
||||
} else {
|
||||
console.log("Unhandled debugger method:", method);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to attach debugger
|
||||
function attachDebugger() {
|
||||
if (isDebuggerAttached) {
|
||||
console.log("Debugger already attached");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the tab still exists
|
||||
chrome.tabs.get(currentTabId, (tab) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Tab no longer exists:", chrome.runtime.lastError);
|
||||
isDebuggerAttached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Attaching debugger to tab:", currentTabId);
|
||||
chrome.debugger.attach({ tabId: currentTabId }, "1.3", () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Failed to attach debugger:", chrome.runtime.lastError);
|
||||
isDebuggerAttached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isDebuggerAttached = true;
|
||||
console.log("Debugger successfully attached");
|
||||
|
||||
// Add the event listener when attaching
|
||||
chrome.debugger.onEvent.addListener(consoleMessageListener);
|
||||
|
||||
chrome.debugger.sendCommand(
|
||||
{ tabId: currentTabId },
|
||||
"Console.enable",
|
||||
{},
|
||||
() => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error(
|
||||
"Failed to enable console:",
|
||||
chrome.runtime.lastError
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("Console API successfully enabled");
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to detach debugger
|
||||
function detachDebugger() {
|
||||
if (!isDebuggerAttached) return;
|
||||
|
||||
// Remove the event listener when detaching
|
||||
chrome.debugger.onEvent.removeListener(consoleMessageListener);
|
||||
|
||||
// Check if debugger is actually attached before trying to detach
|
||||
chrome.debugger.getTargets((targets) => {
|
||||
const isStillAttached = targets.some(
|
||||
(target) => target.tabId === currentTabId && target.attached
|
||||
);
|
||||
|
||||
if (!isStillAttached) {
|
||||
console.log("Debugger already detached");
|
||||
isDebuggerAttached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.debugger.detach({ tabId: currentTabId }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Failed to detach debugger:", chrome.runtime.lastError);
|
||||
isDebuggerAttached = false;
|
||||
return;
|
||||
}
|
||||
isDebuggerAttached = false;
|
||||
console.log("Debugger detached");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Use DevTools Protocol to capture console logs
|
||||
chrome.devtools.panels.create("Browser Logs", "", "panel.html", (panel) => {
|
||||
// Initial attach - we'll keep the debugger attached as long as DevTools is open
|
||||
attachDebugger();
|
||||
|
||||
// Remove the panel show/hide listeners since we want to stay attached
|
||||
// panel.onShown.addListener(() => {
|
||||
// attachDebugger();
|
||||
// });
|
||||
//
|
||||
// panel.onHidden.addListener(() => {
|
||||
// detachDebugger();
|
||||
// });
|
||||
});
|
||||
|
||||
// Clean up only when the entire DevTools window is closed
|
||||
window.addEventListener("unload", () => {
|
||||
detachDebugger();
|
||||
});
|
||||
18
chrome-extension/manifest.json
Normal file
18
chrome-extension/manifest.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "AI Browser Connector",
|
||||
"version": "1.0.0",
|
||||
"description": "Captures console and network logs then sends them to a local aggregator for MCP usage",
|
||||
"manifest_version": 3,
|
||||
"devtools_page": "devtools.html",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"scripting",
|
||||
"debugger",
|
||||
"storage",
|
||||
"tabs"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
|
||||
108
chrome-extension/panel.html
Normal file
108
chrome-extension/panel.html
Normal file
@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
padding: 16px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background-color: #282828;
|
||||
color: #fff;
|
||||
}
|
||||
.endpoint-list {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.endpoint-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
input {
|
||||
padding: 4px;
|
||||
}
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.status-connected {
|
||||
background: #4caf50;
|
||||
}
|
||||
.status-disconnected {
|
||||
background: #f44336;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.checkbox-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
input[type="number"],
|
||||
input[type="text"] {
|
||||
padding: 4px;
|
||||
width: 200px;
|
||||
}
|
||||
.settings-section {
|
||||
border: 1px solid #ccc;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="settings-section">
|
||||
<h3>Log Settings</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="log-limit">Log Limit (number of logs)</label>
|
||||
<input type="number" id="log-limit" min="1" value="50">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="query-limit">Query Limit (characters)</label>
|
||||
<input type="number" id="query-limit" min="1" value="30000">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="string-size-limit">String Size Limit (characters)</label>
|
||||
<input type="number" id="string-size-limit" min="1" value="500">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max-log-size">Max Log Size (characters)</label>
|
||||
<input type="number" id="max-log-size" min="1000" value="20000">
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="show-request-headers">
|
||||
Include Request Headers
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" id="show-response-headers">
|
||||
Include Response Headers
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
80
chrome-extension/panel.js
Normal file
80
chrome-extension/panel.js
Normal file
@ -0,0 +1,80 @@
|
||||
// Store settings
|
||||
let settings = {
|
||||
logLimit: 50,
|
||||
queryLimit: 30000,
|
||||
stringSizeLimit: 500,
|
||||
showRequestHeaders: false,
|
||||
showResponseHeaders: false,
|
||||
maxLogSize: 20000,
|
||||
};
|
||||
|
||||
// Load saved settings on startup
|
||||
chrome.storage.local.get(["browserConnectorSettings"], (result) => {
|
||||
if (result.browserConnectorSettings) {
|
||||
settings = { ...settings, ...result.browserConnectorSettings };
|
||||
updateUIFromSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize UI elements
|
||||
const logLimitInput = document.getElementById("log-limit");
|
||||
const queryLimitInput = document.getElementById("query-limit");
|
||||
const stringSizeLimitInput = document.getElementById("string-size-limit");
|
||||
const showRequestHeadersCheckbox = document.getElementById(
|
||||
"show-request-headers"
|
||||
);
|
||||
const showResponseHeadersCheckbox = document.getElementById(
|
||||
"show-response-headers"
|
||||
);
|
||||
const maxLogSizeInput = document.getElementById("max-log-size");
|
||||
|
||||
// Update UI from settings
|
||||
function updateUIFromSettings() {
|
||||
logLimitInput.value = settings.logLimit;
|
||||
queryLimitInput.value = settings.queryLimit;
|
||||
stringSizeLimitInput.value = settings.stringSizeLimit;
|
||||
showRequestHeadersCheckbox.checked = settings.showRequestHeaders;
|
||||
showResponseHeadersCheckbox.checked = settings.showResponseHeaders;
|
||||
maxLogSizeInput.value = settings.maxLogSize;
|
||||
}
|
||||
|
||||
// Save settings
|
||||
function saveSettings() {
|
||||
chrome.storage.local.set({ browserConnectorSettings: settings });
|
||||
// Notify devtools.js about settings change
|
||||
chrome.runtime.sendMessage({
|
||||
type: "SETTINGS_UPDATED",
|
||||
settings,
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners for all inputs
|
||||
logLimitInput.addEventListener("change", (e) => {
|
||||
settings.logLimit = parseInt(e.target.value, 10);
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
queryLimitInput.addEventListener("change", (e) => {
|
||||
settings.queryLimit = parseInt(e.target.value, 10);
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
stringSizeLimitInput.addEventListener("change", (e) => {
|
||||
settings.stringSizeLimit = parseInt(e.target.value, 10);
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
showRequestHeadersCheckbox.addEventListener("change", (e) => {
|
||||
settings.showRequestHeaders = e.target.checked;
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
showResponseHeadersCheckbox.addEventListener("change", (e) => {
|
||||
settings.showResponseHeaders = e.target.checked;
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
maxLogSizeInput.addEventListener("change", (e) => {
|
||||
settings.maxLogSize = parseInt(e.target.value, 10);
|
||||
saveSettings();
|
||||
});
|
||||
1008
mcp-server/package-lock.json
generated
Normal file
1008
mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
mcp-server/package.json
Normal file
29
mcp-server/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "mcp-server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "tsc && node dist/browser-connector.js",
|
||||
"mcp": "tsc && node dist/mcp-server.js",
|
||||
"inspect": "tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.4.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"llm-cost": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
210
mcp-server/src/browser-connector.ts
Normal file
210
mcp-server/src/browser-connector.ts
Normal file
@ -0,0 +1,210 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import bodyParser from "body-parser";
|
||||
import { tokenizeAndEstimateCost } from "llm-cost";
|
||||
|
||||
// We store logs in memory
|
||||
const consoleLogs: any[] = [];
|
||||
const consoleErrors: any[] = [];
|
||||
const networkErrors: any[] = [];
|
||||
const networkSuccess: any[] = [];
|
||||
const allXhr: any[] = [];
|
||||
|
||||
// Add settings state
|
||||
let currentSettings = {
|
||||
logLimit: 50,
|
||||
queryLimit: 30000,
|
||||
showRequestHeaders: false,
|
||||
showResponseHeaders: false,
|
||||
model: "claude-3-sonnet",
|
||||
stringSizeLimit: 500,
|
||||
maxLogSize: 20000,
|
||||
};
|
||||
|
||||
const app = express();
|
||||
const PORT = 3025;
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// Helper to recursively truncate strings in any data structure
|
||||
function truncateStringsInData(data: any, maxLength: number): any {
|
||||
if (typeof data === "string") {
|
||||
return data.length > maxLength
|
||||
? data.substring(0, maxLength) + "... (truncated)"
|
||||
: data;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) => truncateStringsInData(item, maxLength));
|
||||
}
|
||||
|
||||
if (typeof data === "object" && data !== null) {
|
||||
const result: any = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[key] = truncateStringsInData(value, maxLength);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Helper to safely parse and process JSON strings
|
||||
function processJsonString(jsonString: string, maxLength: number): string {
|
||||
try {
|
||||
// Try to parse the string as JSON
|
||||
const parsed = JSON.parse(jsonString);
|
||||
// Process any strings within the parsed JSON
|
||||
const processed = truncateStringsInData(parsed, maxLength);
|
||||
// Stringify the processed data
|
||||
return JSON.stringify(processed);
|
||||
} catch (e) {
|
||||
// If it's not valid JSON, treat it as a regular string
|
||||
return truncateStringsInData(jsonString, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to process logs based on settings
|
||||
function processLogsWithSettings(logs: any[]) {
|
||||
return logs.map((log) => {
|
||||
const processedLog = { ...log };
|
||||
|
||||
if (log.type === "network-request") {
|
||||
// Handle headers visibility
|
||||
if (!currentSettings.showRequestHeaders) {
|
||||
delete processedLog.requestHeaders;
|
||||
}
|
||||
if (!currentSettings.showResponseHeaders) {
|
||||
delete processedLog.responseHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
return processedLog;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to calculate size of a log entry
|
||||
function calculateLogSize(log: any): number {
|
||||
return JSON.stringify(log).length;
|
||||
}
|
||||
|
||||
// Helper to truncate logs based on character limit
|
||||
function truncateLogsToQueryLimit(logs: any[]): any[] {
|
||||
if (logs.length === 0) return logs;
|
||||
|
||||
// First process logs according to current settings
|
||||
const processedLogs = processLogsWithSettings(logs);
|
||||
|
||||
let currentSize = 0;
|
||||
const result = [];
|
||||
|
||||
for (const log of processedLogs) {
|
||||
const logSize = calculateLogSize(log);
|
||||
|
||||
// Check if adding this log would exceed the limit
|
||||
if (currentSize + logSize > currentSettings.queryLimit) {
|
||||
console.log(
|
||||
`Reached query limit (${currentSize}/${currentSettings.queryLimit}), truncating logs`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add log and update size
|
||||
result.push(log);
|
||||
currentSize += logSize;
|
||||
console.log(`Added log of size ${logSize}, total size now: ${currentSize}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Endpoint for the extension to POST data
|
||||
app.post("/extension-log", (req, res) => {
|
||||
const { data, settings } = req.body;
|
||||
|
||||
// Update settings if provided
|
||||
if (settings) {
|
||||
currentSettings = {
|
||||
...currentSettings,
|
||||
...settings,
|
||||
};
|
||||
}
|
||||
|
||||
console.log("Received log:", data);
|
||||
switch (data.type) {
|
||||
case "console-log":
|
||||
consoleLogs.push(data);
|
||||
if (consoleLogs.length > currentSettings.logLimit) consoleLogs.shift();
|
||||
break;
|
||||
case "console-error":
|
||||
consoleErrors.push(data);
|
||||
if (consoleErrors.length > currentSettings.logLimit)
|
||||
consoleErrors.shift();
|
||||
break;
|
||||
case "network-request":
|
||||
// Route network requests based on status code
|
||||
if (data.status >= 400) {
|
||||
networkErrors.push(data);
|
||||
if (networkErrors.length > currentSettings.logLimit)
|
||||
networkErrors.shift();
|
||||
} else {
|
||||
networkSuccess.push(data);
|
||||
if (networkSuccess.length > currentSettings.logLimit)
|
||||
networkSuccess.shift();
|
||||
}
|
||||
break;
|
||||
}
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
// Update GET endpoints to use the new function
|
||||
app.get("/console-logs", (req, res) => {
|
||||
const truncatedLogs = truncateLogsToQueryLimit(consoleLogs);
|
||||
res.json(truncatedLogs);
|
||||
});
|
||||
|
||||
app.get("/console-errors", (req, res) => {
|
||||
const truncatedLogs = truncateLogsToQueryLimit(consoleErrors);
|
||||
res.json(truncatedLogs);
|
||||
});
|
||||
|
||||
app.get("/network-errors", (req, res) => {
|
||||
const truncatedLogs = truncateLogsToQueryLimit(networkErrors);
|
||||
res.json(truncatedLogs);
|
||||
});
|
||||
|
||||
app.get("/network-success", (req, res) => {
|
||||
const truncatedLogs = truncateLogsToQueryLimit(networkSuccess);
|
||||
res.json(truncatedLogs);
|
||||
});
|
||||
|
||||
app.get("/all-xhr", (req, res) => {
|
||||
// Merge and sort network success and error logs by timestamp
|
||||
const mergedLogs = [...networkSuccess, ...networkErrors].sort(
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
const truncatedLogs = truncateLogsToQueryLimit(mergedLogs);
|
||||
res.json(truncatedLogs);
|
||||
});
|
||||
|
||||
app.get("/.port", (req, res) => {
|
||||
res.send(PORT.toString());
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Aggregator listening on http://127.0.0.1:${PORT}`);
|
||||
|
||||
// Write the port to a file so mcp-server can read it
|
||||
const fs = require("fs");
|
||||
fs.writeFileSync(".port", PORT.toString());
|
||||
});
|
||||
|
||||
// Handle shutdown gracefully
|
||||
process.on("SIGINT", () => {
|
||||
server.close(() => {
|
||||
console.log("Server shut down");
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
105
mcp-server/src/mcp-server.ts
Normal file
105
mcp-server/src/mcp-server.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import fs from "fs";
|
||||
|
||||
// Create the MCP server
|
||||
const server = new McpServer({
|
||||
name: "AI Browser Connector",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Function to get the port from the .port file
|
||||
function getPort(): number {
|
||||
try {
|
||||
const port = parseInt(fs.readFileSync(".port", "utf8"));
|
||||
return port;
|
||||
} catch (err) {
|
||||
console.error("Could not read port file, defaulting to 3000");
|
||||
return 3025;
|
||||
}
|
||||
}
|
||||
|
||||
// const PORT = getPort();
|
||||
|
||||
const PORT = 3025;
|
||||
|
||||
// We'll define four "tools" that retrieve data from the aggregator at localhost:3000
|
||||
|
||||
server.tool("getConsoleLogs", "Check our browser logs", async () => {
|
||||
const response = await fetch(`http://127.0.0.1:${PORT}/console-logs`);
|
||||
const json = await response.json();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(json, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// server.tool(
|
||||
// "getConsoleErrors",
|
||||
// "Check our browsers console errors",
|
||||
// async () => {
|
||||
// const response = await fetch(`http://127.0.0.1:${PORT}/console-errors`);
|
||||
// const json = await response.json();
|
||||
// const last50 = json.slice(-50);
|
||||
// return {
|
||||
// content: [
|
||||
// {
|
||||
// type: "text",
|
||||
// text: JSON.stringify(last50, null, 2),
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
// }
|
||||
// );
|
||||
|
||||
// Return all HTTP errors (4xx/5xx)
|
||||
server.tool("getNetworkErrors", "Check our network ERROR logs", async () => {
|
||||
const response = await fetch(`http://127.0.0.1:${PORT}/network-errors`);
|
||||
const json = await response.json();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(json, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Return all XHR/fetch requests
|
||||
server.tool("getNetworkSuccess", "Check our network SUCCESS logs", async () => {
|
||||
const response = await fetch(`http://127.0.0.1:${PORT}/all-xhr`);
|
||||
const json = await response.json();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(json, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Return all XHR/fetch requests
|
||||
server.tool("getNetworkLogs", "Check ALL our network logs", async () => {
|
||||
const response = await fetch(`http://127.0.0.1:${PORT}/all-xhr`);
|
||||
const json = await response.json();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(json, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Start receiving messages on stdio
|
||||
(async () => {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
})();
|
||||
111
mcp-server/tsconfig.json
Normal file
111
mcp-server/tsconfig.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user