@webeyez/mcp-server 1.0.1
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 +72 -0
- package/dist/claude-proxy.js +377 -0
- package/dist/index.js +146 -0
- package/dist/install-env.js +83 -0
- package/dist/setup.js +56 -0
- package/dist/tools/agents.proto +20 -0
- package/dist/tools/auth.js +138 -0
- package/dist/tools/job-store.js +20 -0
- package/dist/tools/organization.proto +102 -0
- package/dist/tools/public.js +426 -0
- package/dist/utils/paths.js +22 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Webeyez Model Context Protocol (MCP) Server
|
|
2
|
+
|
|
3
|
+
Access Webeyez session replays, JavaScript errors, conversion funnels, and performance diagnostics directly from any MCP-compatible AI client.
|
|
4
|
+
|
|
5
|
+
## What is Webeyez?
|
|
6
|
+
Webeyez is a comprehensive real-time user experience monitoring and diagnostic platform that helps online businesses detect, investigate, and resolve issues impacting their users. By tracking user friction, client-side errors, and drop-offs, Webeyez pinpoints exact conversion and revenue leaks.
|
|
7
|
+
|
|
8
|
+
**All data, insights, and opportunities collected by Webeyez are available through the Model Context Protocol (MCP).** This allows your AI agents and assistants to query live performance data, search session recordings, inspect JavaScript logs, and analyze conversion drop-offs directly within your chat environment.
|
|
9
|
+
|
|
10
|
+
This server implements the Model Context Protocol (MCP) to seamlessly expose Webeyez's core analytics tools into AI clients such as **Claude (Web App)**, **Claude Desktop**, **Cursor**, **Windsurf**, **Cline**, and any other MCP-compliant interface.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @webeyez/mcp-server
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Connection Setup
|
|
19
|
+
|
|
20
|
+
The Webeyez MCP server supports both SSE (Server-Sent Events) for web applications and stdio/SSE bridging for local desktop editors.
|
|
21
|
+
|
|
22
|
+
### Option 1: SSE Configuration (e.g., Claude Web App)
|
|
23
|
+
Register the server as a custom connector in web-based clients:
|
|
24
|
+
1. Go to **Settings > Connectors > Add custom connector** (or equivalent in your AI client).
|
|
25
|
+
2. Enter the connector URL: `https://api.app.webeyez.com/mcp`
|
|
26
|
+
3. Log in with your Webeyez credentials when prompted.
|
|
27
|
+
|
|
28
|
+
### Option 2: Stdio Bridge Configuration (e.g., Claude Desktop, Cursor, Windsurf)
|
|
29
|
+
Since local editor applications expect local processes, connect them to the remote server using the official `mcp-remote` bridge tool.
|
|
30
|
+
|
|
31
|
+
#### For Claude Desktop:
|
|
32
|
+
Configure your `claude_desktop_config.json`:
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"webeyez": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": [
|
|
39
|
+
"-y",
|
|
40
|
+
"mcp-remote@latest",
|
|
41
|
+
"https://api.app.webeyez.com/mcp"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### For Cursor:
|
|
49
|
+
1. Go to **Settings > Features > MCP**.
|
|
50
|
+
2. Click **+ Add New MCP Server**.
|
|
51
|
+
3. Set **Name** to `webeyez`.
|
|
52
|
+
4. Set **Type** to `command`.
|
|
53
|
+
5. Set **Command** to:
|
|
54
|
+
```bash
|
|
55
|
+
npx -y mcp-remote@latest https://api.app.webeyez.com/mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Available Tools
|
|
59
|
+
|
|
60
|
+
All tools require standard authentication (which is handled automatically via OAuth on the connection setup):
|
|
61
|
+
|
|
62
|
+
* `list_accounts`: List all Webeyez accounts the authenticated user has access to.
|
|
63
|
+
* `list_sites`: List all configured sites under your accounts.
|
|
64
|
+
* `get_site_status`: Check tracking and integration status for a specific site.
|
|
65
|
+
* `get_revenue_loss_estimate`: Retrieve estimated conversion and revenue losses.
|
|
66
|
+
* `get_funnel_summary`: Fetch conversion funnel stages and drops.
|
|
67
|
+
* `get_checkout_dropoffs`: Identify checkout friction points.
|
|
68
|
+
* `get_js_errors`: Fetch javascript console and runtime errors.
|
|
69
|
+
* `get_failed_api_calls`: Retrieve failing HTTP/network requests.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
ISC License
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const { EventSource } = require('eventsource');
|
|
41
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
42
|
+
const readline_1 = __importDefault(require("readline"));
|
|
43
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
44
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
45
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
46
|
+
const zod_1 = require("zod");
|
|
47
|
+
const paths_1 = require("./utils/paths");
|
|
48
|
+
// Retrieve the token and gateway URL from environment variables
|
|
49
|
+
const API_KEY = process.env.WEBEYEZ_API_KEY;
|
|
50
|
+
const GATEWAY_URL = process.env.GATEWAY_URL || 'https://api.app.webeyez.com';
|
|
51
|
+
async function startFallbackServer(errorMsg = "Authentication Required: Your Personal Access Token (PAT) is missing. To get a new PAT, go to https://app.webeyez.com -> click the top right user icon -> Settings -> User Settings tab -> Personal Access Token section. Once the user provides the token, use the `login_webeyez` tool to save it and log in.") {
|
|
52
|
+
const mcp = new mcp_js_1.McpServer({
|
|
53
|
+
name: "Webeyez-MCP-Unauthenticated",
|
|
54
|
+
version: "1.0.0"
|
|
55
|
+
});
|
|
56
|
+
mcp.tool("ask_webeyez", "Free text for querying the data webeyez collected for the desired domain in that current organization. Webeyez identifies all JS errors, API failed calls, core web vitals, sessions, page views, frustration clicks, and many more related to client side analytics as webeyez has the tracker on the site. CRITICAL INSTRUCTION FOR CLAUDE: If this tool returns an Authentication error, you MUST copy and paste the EXACT error message to the user word-for-word. Do NOT paraphrase the error message, as it contains precise, technical UI instructions on how the user can generate a new Personal Access Token.", {
|
|
57
|
+
question: zod_1.z.string().describe("The free text query / question to ask the AI"),
|
|
58
|
+
domain: zod_1.z.string().optional().describe("The domain context for the question")
|
|
59
|
+
}, async () => {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: errorMsg }]
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
mcp.tool("login_webeyez", "Securely log in to Webeyez. If you do not provide a 'pat' parameter, it will automatically initiate an OAuth 2.1 authentication flow and open your default browser to sign in. CRITICAL: Once you call this tool, the server will restart. You MUST STOP your response immediately after calling this tool. Do NOT call any other tools (like ask_webeyez). Just tell the user to follow the browser login tab, wait a few seconds, and then retry their query.", {
|
|
65
|
+
pat: zod_1.z.string().optional().describe("Optional: Manually paste a Personal Access Token (PAT) if browser redirect fails.")
|
|
66
|
+
}, async ({ pat }) => {
|
|
67
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
68
|
+
const http = await Promise.resolve().then(() => __importStar(require('http')));
|
|
69
|
+
const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
70
|
+
const { exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
71
|
+
const configPath = (0, paths_1.getClaudeConfigPath)();
|
|
72
|
+
if (pat) {
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(configPath)) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: "Configuration file not found. Please create it manually." }],
|
|
77
|
+
isError: true
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const configData = fs.readFileSync(configPath, 'utf8');
|
|
81
|
+
const config = JSON.parse(configData);
|
|
82
|
+
if (!config.mcpServers || !config.mcpServers["wz-mcp-service"]) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: "wz-mcp-service not found in configuration file." }],
|
|
85
|
+
isError: true
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!config.mcpServers["wz-mcp-service"].env) {
|
|
89
|
+
config.mcpServers["wz-mcp-service"].env = {};
|
|
90
|
+
}
|
|
91
|
+
config.mcpServers['wz-mcp-service'].env.WEBEYEZ_API_KEY = String(pat);
|
|
92
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
93
|
+
setTimeout(() => process.exit(0), 1000);
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: "Successfully saved the PAT to the configuration! Claude Desktop is now restarting the server. Please wait 3 seconds and then retry your query." }]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `Failed to update configuration: ${err.message}` }],
|
|
101
|
+
isError: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Auto-login OAuth flow
|
|
106
|
+
try {
|
|
107
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
108
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest().toString('base64url');
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const server = http.createServer(async (req, res) => {
|
|
111
|
+
const reqUrl = new URL(req.url || '', `http://${req.headers.host}`);
|
|
112
|
+
if (reqUrl.pathname !== '/callback') {
|
|
113
|
+
res.writeHead(404);
|
|
114
|
+
res.end('Not Found');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const code = reqUrl.searchParams.get('code');
|
|
118
|
+
if (!code) {
|
|
119
|
+
res.writeHead(400);
|
|
120
|
+
res.end('Authorization code not found in request');
|
|
121
|
+
server.close();
|
|
122
|
+
resolve({
|
|
123
|
+
content: [{ type: "text", text: "Authentication failed: No authorization code received." }],
|
|
124
|
+
isError: true
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Exchange code for token
|
|
129
|
+
try {
|
|
130
|
+
const tokenUrl = `${GATEWAY_URL.replace(/\/$/, '')}/v2/auth/oauth/token`;
|
|
131
|
+
const tokenResponse = await (0, node_fetch_1.default)(tokenUrl, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
grant_type: 'authorization_code',
|
|
136
|
+
code,
|
|
137
|
+
redirect_uri: 'http://localhost:5053/callback',
|
|
138
|
+
client_id: 'claude-desktop-client',
|
|
139
|
+
code_verifier: codeVerifier
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
if (!tokenResponse.ok) {
|
|
143
|
+
const errBody = await tokenResponse.text();
|
|
144
|
+
res.writeHead(400);
|
|
145
|
+
res.end(`Token exchange failed: ${errBody}`);
|
|
146
|
+
server.close();
|
|
147
|
+
resolve({
|
|
148
|
+
content: [{ type: "text", text: `Token exchange failed: ${errBody}` }],
|
|
149
|
+
isError: true
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const tokenData = await tokenResponse.json();
|
|
154
|
+
const accessToken = tokenData.access_token;
|
|
155
|
+
if (!accessToken) {
|
|
156
|
+
res.writeHead(400);
|
|
157
|
+
res.end('Invalid response from token endpoint');
|
|
158
|
+
server.close();
|
|
159
|
+
resolve({
|
|
160
|
+
content: [{ type: "text", text: "Failed to obtain access token." }],
|
|
161
|
+
isError: true
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Update config
|
|
166
|
+
const configData = fs.readFileSync(configPath, 'utf8');
|
|
167
|
+
const config = JSON.parse(configData);
|
|
168
|
+
if (!config.mcpServers["wz-mcp-service"].env) {
|
|
169
|
+
config.mcpServers["wz-mcp-service"].env = {};
|
|
170
|
+
}
|
|
171
|
+
config.mcpServers['wz-mcp-service'].env.WEBEYEZ_API_KEY = accessToken;
|
|
172
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
173
|
+
// Success response HTML page
|
|
174
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
175
|
+
res.end(`<!DOCTYPE html>
|
|
176
|
+
<html>
|
|
177
|
+
<head>
|
|
178
|
+
<title>Login Successful | Webeyez MCP</title>
|
|
179
|
+
<style>
|
|
180
|
+
body { background: #09090e; color: #f8fafc; font-family: system-ui, -apple-system, sans-serif; text-align: center; padding-top: 100px; margin: 0; }
|
|
181
|
+
.card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 20px; display: inline-block; padding: 40px 60px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); backdrop-filter: blur(10px); }
|
|
182
|
+
h1 { color: #00E08F; font-size: 2.2rem; margin-bottom: 10px; }
|
|
183
|
+
p { color: #94a3b8; font-size: 1.1rem; line-height: 1.6; }
|
|
184
|
+
.checkmark { font-size: 4rem; color: #00E08F; margin-bottom: 20px; }
|
|
185
|
+
</style>
|
|
186
|
+
</head>
|
|
187
|
+
<body>
|
|
188
|
+
<div class="card">
|
|
189
|
+
<div class="checkmark">✓</div>
|
|
190
|
+
<h1>Login Successful!</h1>
|
|
191
|
+
<p>You have successfully logged in to Webeyez MCP.</p>
|
|
192
|
+
<p>You can close this browser tab and return to Claude Desktop now.</p>
|
|
193
|
+
</div>
|
|
194
|
+
</body>
|
|
195
|
+
</html>`);
|
|
196
|
+
// Gracefully stop server and exit process
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
server.close();
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}, 1000);
|
|
201
|
+
resolve({
|
|
202
|
+
content: [{ type: "text", text: "Successfully logged in via OAuth! Claude Desktop is restarting the server." }]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
res.writeHead(500);
|
|
207
|
+
res.end(`Internal Server Error: ${err.message}`);
|
|
208
|
+
server.close();
|
|
209
|
+
resolve({
|
|
210
|
+
content: [{ type: "text", text: `Authentication error during exchange: ${err.message}` }],
|
|
211
|
+
isError: true
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
server.listen(5053, '127.0.0.1', () => {
|
|
216
|
+
const authorizeUrl = `${GATEWAY_URL.replace(/\/$/, '')}/v2/auth/oauth/authorize?response_type=code&client_id=claude-desktop-client&redirect_uri=http://localhost:5053/callback&state=state_123&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
|
217
|
+
const startCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
218
|
+
exec(`${startCmd} "${authorizeUrl}"`, (err) => {
|
|
219
|
+
if (err) {
|
|
220
|
+
console.error(`[Proxy] Failed to open browser: ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// Set a timeout of 3 minutes for user input
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
server.close();
|
|
227
|
+
resolve({
|
|
228
|
+
content: [{ type: "text", text: "Login failed: Authentication timed out after 3 minutes." }],
|
|
229
|
+
isError: true
|
|
230
|
+
});
|
|
231
|
+
}, 3 * 60 * 1000);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: `Failed to initiate OAuth login: ${err.message}` }],
|
|
237
|
+
isError: true
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
242
|
+
await mcp.connect(transport);
|
|
243
|
+
}
|
|
244
|
+
function isTokenValid(token) {
|
|
245
|
+
if (!token || token === 'no-pat')
|
|
246
|
+
return false;
|
|
247
|
+
try {
|
|
248
|
+
const parts = token.split('.');
|
|
249
|
+
if (parts.length !== 3)
|
|
250
|
+
return false;
|
|
251
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
|
|
252
|
+
if (payload.exp) {
|
|
253
|
+
// exp is in seconds, Date.now() is in milliseconds
|
|
254
|
+
if (payload.exp * 1000 < Date.now()) {
|
|
255
|
+
console.error("[Proxy] PAT has expired locally.");
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!API_KEY || !isTokenValid(API_KEY)) {
|
|
266
|
+
// Start a local fallback server that just returns auth instructions
|
|
267
|
+
startFallbackServer("Authentication Failed: Your Personal Access Token (PAT) is invalid, malformed, or expired. To get a new PAT, go to https://app.webeyez.com -> click the top right user icon -> Settings -> User Settings tab -> Personal Access Token section. Once you provide the new token, use the `login_webeyez` tool to save it and log in.").catch(console.error);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
checkConnectionAndStart();
|
|
271
|
+
}
|
|
272
|
+
async function checkConnectionAndStart() {
|
|
273
|
+
const GATEWAY_ENDPOINT = `${GATEWAY_URL}/mcp/`;
|
|
274
|
+
const controller = new AbortController();
|
|
275
|
+
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
276
|
+
try {
|
|
277
|
+
// Test connection to the /mcp endpoint with a simple invalid message (since we just want to verify HTTP/auth status)
|
|
278
|
+
const response = await (0, node_fetch_1.default)(GATEWAY_ENDPOINT, {
|
|
279
|
+
method: 'POST',
|
|
280
|
+
headers: {
|
|
281
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
'Accept': 'application/json, text/event-stream'
|
|
284
|
+
},
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
jsonrpc: '2.0',
|
|
287
|
+
method: 'ping',
|
|
288
|
+
id: 'ping-id'
|
|
289
|
+
}),
|
|
290
|
+
signal: controller.signal
|
|
291
|
+
});
|
|
292
|
+
clearTimeout(timeoutId);
|
|
293
|
+
console.error(`[Proxy] Fetch status: ${response.status} ${response.statusText}`);
|
|
294
|
+
if (response.status === 401 || response.status === 403) {
|
|
295
|
+
if (response.body && response.body.destroy)
|
|
296
|
+
response.body.destroy();
|
|
297
|
+
await startFallbackServer("Authentication Failed: Your Personal Access Token (PAT) is invalid or expired. To get a new PAT, go to https://app.webeyez.com -> click the top right user icon -> Settings -> User Settings tab -> Personal Access Token section. Once the user provides the new token, use the `login_webeyez` tool to save it and log in.");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!response.ok && response.status !== 400) {
|
|
301
|
+
if (response.body && response.body.destroy)
|
|
302
|
+
response.body.destroy();
|
|
303
|
+
console.error(`The Webeyez servers are currently not responding (Status: ${response.status}). Exiting so Claude can retry.`);
|
|
304
|
+
await startFallbackServer(`Connection to Gateway failed with HTTP Status: ${response.status}. Please check if the Gateway is running and the /mcp endpoint is available.`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (response.body && response.body.destroy)
|
|
308
|
+
response.body.destroy();
|
|
309
|
+
startProxyClient();
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
clearTimeout(timeoutId);
|
|
313
|
+
console.error(`The Webeyez servers are currently not responding or unreachable. Error: ${err.message}. Exiting so Claude can retry.`);
|
|
314
|
+
await startFallbackServer(`Connection to Gateway failed entirely. Error: ${err.message}. Please check if the Gateway is running at ${GATEWAY_URL} and reachable.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function startProxyClient() {
|
|
318
|
+
const GATEWAY_ENDPOINT = `${GATEWAY_URL}/mcp/`;
|
|
319
|
+
// Create StreamableHTTP client transport
|
|
320
|
+
const clientTransport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(GATEWAY_ENDPOINT), {
|
|
321
|
+
requestInit: {
|
|
322
|
+
headers: {
|
|
323
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
324
|
+
'x-api-key': API_KEY || ''
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
clientTransport.onmessage = (message) => {
|
|
329
|
+
// Write JSON-RPC string back to Claude's stdout
|
|
330
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
331
|
+
};
|
|
332
|
+
clientTransport.onerror = async (err) => {
|
|
333
|
+
console.error('[Proxy] Client transport error:', err);
|
|
334
|
+
// If we get an authentication error during active usage, clear the token and exit to fallback
|
|
335
|
+
if (err?.code === 401 || err?.code === 403 || err?.message?.includes('401') || err?.message?.includes('403')) {
|
|
336
|
+
console.error('[Proxy] Access token was rejected by server. Clearing token...');
|
|
337
|
+
try {
|
|
338
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
339
|
+
const configPath = (0, paths_1.getClaudeConfigPath)();
|
|
340
|
+
if (fs.existsSync(configPath)) {
|
|
341
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
342
|
+
if (config.mcpServers && config.mcpServers["wz-mcp-service"] && config.mcpServers["wz-mcp-service"].env) {
|
|
343
|
+
delete config.mcpServers["wz-mcp-service"].env.WEBEYEZ_API_KEY;
|
|
344
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
console.error('[Proxy] Failed to clear token:', e);
|
|
350
|
+
}
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
clientTransport.onclose = () => {
|
|
355
|
+
console.error('[Proxy] Connection closed.');
|
|
356
|
+
process.exit(0);
|
|
357
|
+
};
|
|
358
|
+
// Forward outgoing messages from Claude's stdin to the remote Server
|
|
359
|
+
const rl = readline_1.default.createInterface({
|
|
360
|
+
input: process.stdin,
|
|
361
|
+
output: process.stdout,
|
|
362
|
+
terminal: false
|
|
363
|
+
});
|
|
364
|
+
rl.on('line', async (line) => {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(line);
|
|
367
|
+
await clientTransport.send(parsed);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
console.error('[Proxy] Failed to parse or send line:', err);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
clientTransport.start().catch((err) => {
|
|
374
|
+
console.error('[Proxy] Failed to start client transport:', err);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
|
377
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const cors_1 = __importDefault(require("cors"));
|
|
8
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
11
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
12
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
|
+
const public_1 = require("./tools/public");
|
|
14
|
+
const net_1 = __importDefault(require("net"));
|
|
15
|
+
// Redirect console.log to console.error in stdio mode so random logs don't break JSON-RPC
|
|
16
|
+
if (process.argv.includes('stdio')) {
|
|
17
|
+
console.log = console.error;
|
|
18
|
+
}
|
|
19
|
+
dotenv_1.default.config();
|
|
20
|
+
const app = (0, express_1.default)();
|
|
21
|
+
app.use((0, cors_1.default)());
|
|
22
|
+
app.use(express_1.default.json()); // Essential for parsing incoming MCP messages
|
|
23
|
+
const transports = new Map();
|
|
24
|
+
// Standardized Streamable HTTP endpoint for remote clients
|
|
25
|
+
app.all(['/mcp', '/mcp/*splat'], async (req, res) => {
|
|
26
|
+
// 1. Get session ID from headers or query string
|
|
27
|
+
const sessionId = req.headers['mcp-session-id'] || req.query.sessionId;
|
|
28
|
+
if (sessionId) {
|
|
29
|
+
const session = transports.get(sessionId);
|
|
30
|
+
if (!session) {
|
|
31
|
+
res.status(404).json({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
error: { code: -32001, message: `Session not found: ${sessionId}` },
|
|
34
|
+
id: null
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// 2. If no session ID, it MUST be a new session POST containing initialization
|
|
42
|
+
if (req.method !== 'POST') {
|
|
43
|
+
res.status(400).json({
|
|
44
|
+
jsonrpc: "2.0",
|
|
45
|
+
error: { code: -32000, message: "Bad Request: Session ID required for GET/DELETE" },
|
|
46
|
+
id: null
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const body = req.body;
|
|
51
|
+
const isInit = Array.isArray(body)
|
|
52
|
+
? body.some(msg => msg?.method === 'initialize')
|
|
53
|
+
: body?.method === 'initialize';
|
|
54
|
+
if (!isInit) {
|
|
55
|
+
res.status(400).json({
|
|
56
|
+
jsonrpc: "2.0",
|
|
57
|
+
error: { code: -32000, message: "Bad Request: Session ID required" },
|
|
58
|
+
id: null
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Extract PAT from Authorization, x-api-key header, or query string token
|
|
63
|
+
const authHeader = req.headers['authorization'];
|
|
64
|
+
const pat = authHeader && authHeader.startsWith('Bearer ')
|
|
65
|
+
? authHeader.substring(7)
|
|
66
|
+
: req.headers['x-api-key']
|
|
67
|
+
|| req.query.token;
|
|
68
|
+
let customerId = req.headers['x-org-id'] ? parseInt(req.headers['x-org-id'], 10) : undefined;
|
|
69
|
+
const userId = req.headers['x-user-id'];
|
|
70
|
+
// Try to extract orgId from JWT if x-org-id wasn't provided
|
|
71
|
+
if (!customerId && pat && pat !== 'no-pat') {
|
|
72
|
+
try {
|
|
73
|
+
const parts = pat.split('.');
|
|
74
|
+
if (parts.length === 3) {
|
|
75
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
|
|
76
|
+
if (payload?.sub?.orgId) {
|
|
77
|
+
customerId = parseInt(payload.sub.orgId, 10);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) { }
|
|
82
|
+
}
|
|
83
|
+
// Create a new MCP Server instance per connection session
|
|
84
|
+
const mcp = new mcp_js_1.McpServer({
|
|
85
|
+
name: "Webeyez-MCP",
|
|
86
|
+
version: "1.0.0"
|
|
87
|
+
});
|
|
88
|
+
// Setup tools for this instance
|
|
89
|
+
(0, public_1.setupPublicTools)(mcp, { customerId, userId, pat });
|
|
90
|
+
// Create a new StreamableHTTPServerTransport
|
|
91
|
+
let transport;
|
|
92
|
+
transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
93
|
+
sessionIdGenerator: () => crypto_1.default.randomUUID(),
|
|
94
|
+
onsessioninitialized: (sid) => {
|
|
95
|
+
transports.set(sid, { transport, mcp });
|
|
96
|
+
},
|
|
97
|
+
onsessionclosed: (sid) => {
|
|
98
|
+
transports.delete(sid);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await mcp.connect(transport);
|
|
102
|
+
// Let the transport handle the request
|
|
103
|
+
await transport.handleRequest(req, res, req.body);
|
|
104
|
+
});
|
|
105
|
+
// Health check
|
|
106
|
+
app.get('/health-check', (req, res) => {
|
|
107
|
+
res.json({ status: 'ok' });
|
|
108
|
+
});
|
|
109
|
+
// Function to find a free port
|
|
110
|
+
function getFreePort(startingPort) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const server = net_1.default.createServer();
|
|
113
|
+
server.listen(startingPort, () => {
|
|
114
|
+
const port = server.address().port;
|
|
115
|
+
server.close(() => {
|
|
116
|
+
resolve(port);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
server.on('error', () => {
|
|
120
|
+
resolve(getFreePort(startingPort + 1));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function startServer() {
|
|
125
|
+
if (process.argv.includes('stdio')) {
|
|
126
|
+
const mcp = new mcp_js_1.McpServer({
|
|
127
|
+
name: "Webeyez-MCP",
|
|
128
|
+
version: "1.0.0"
|
|
129
|
+
});
|
|
130
|
+
(0, public_1.setupPublicTools)(mcp, {});
|
|
131
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
132
|
+
await mcp.connect(transport);
|
|
133
|
+
console.error("Webeyez MCP Server running on stdio");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const requestedPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
137
|
+
const port = await getFreePort(requestedPort);
|
|
138
|
+
app.listen(port, () => {
|
|
139
|
+
console.log(`Webeyez MCP Server running on port ${port}`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
startServer().catch((err) => {
|
|
143
|
+
console.error("Failed to start server:", err);
|
|
144
|
+
});
|
|
145
|
+
// Keep process alive explicitly
|
|
146
|
+
setInterval(() => { }, 1000 * 60 * 60);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const paths_1 = require("./utils/paths");
|
|
42
|
+
const dotenv = __importStar(require("dotenv"));
|
|
43
|
+
dotenv.config();
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
const targetEnv = args[0] || 'local'; // 'local' or 'production'
|
|
46
|
+
let gatewayUrl = 'http://localhost:5050';
|
|
47
|
+
if (targetEnv === 'local') {
|
|
48
|
+
gatewayUrl = (process.env.GATEWAY_URL || 'http://localhost:5050');
|
|
49
|
+
}
|
|
50
|
+
else if (targetEnv === 'production') {
|
|
51
|
+
// You can replace this with the actual production URL or set PROD_GATEWAY_URL in your .env
|
|
52
|
+
gatewayUrl = (process.env.PROD_GATEWAY_URL || 'https://api.app.webeyez.com');
|
|
53
|
+
}
|
|
54
|
+
const configPath = (0, paths_1.getClaudeConfigPath)();
|
|
55
|
+
try {
|
|
56
|
+
let config = {};
|
|
57
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
58
|
+
config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
59
|
+
}
|
|
60
|
+
if (!config.mcpServers)
|
|
61
|
+
config.mcpServers = {};
|
|
62
|
+
if (!config.mcpServers['wz-mcp-service']) {
|
|
63
|
+
config.mcpServers['wz-mcp-service'] = {
|
|
64
|
+
command: 'node',
|
|
65
|
+
args: [path_1.default.resolve(__dirname, '../dist/claude-proxy.js')],
|
|
66
|
+
env: {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const serverConfig = config.mcpServers['wz-mcp-service'];
|
|
70
|
+
if (!serverConfig.env)
|
|
71
|
+
serverConfig.env = {};
|
|
72
|
+
serverConfig.env.GATEWAY_URL = gatewayUrl;
|
|
73
|
+
// Ensure the command points to the absolute path correctly
|
|
74
|
+
serverConfig.command = 'node';
|
|
75
|
+
serverConfig.args = [path_1.default.resolve(__dirname, '../dist/claude-proxy.js')];
|
|
76
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
77
|
+
console.log(`\n✅ Successfully configured Claude Desktop for '${targetEnv}'!`);
|
|
78
|
+
console.log(`📡 Gateway URL set to: ${gatewayUrl}`);
|
|
79
|
+
console.log(`\n⚠️ CRITICAL: You MUST fully quit Claude Desktop (Cmd+Q) and reopen it for the changes to apply!\n`);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error(`❌ Failed to configure Claude Desktop: ${error.message}`);
|
|
83
|
+
}
|