Fork de @jason.today/webmcp v0.1.13 con parches Nucleo: registro dinamico, clipboard y clear-cache
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Jason McGhee
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
141
README.md
Normal file
141
README.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# WebMCP
|
||||||
|
|
||||||
|
A proposal and code for websites to support client side LLMs
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@jason.today/webmcp) [](./LICENSE)
|
||||||
|
|
||||||
|
WebMCP allows websites to share tools, resources, prompts, etc. to LLMs. In other words, WebMCP allows a website to be an MCP server. No sharing API Keys. Use any model you want.
|
||||||
|
|
||||||
|
[Here's a simple website I built that is WebMCP-enabled](https://webmcp.dev)
|
||||||
|
|
||||||
|
It comes in the form of a widget that a website owner can put on their site and expose tools to give client-side LLMs what they need to provide a great UX for the user or agent.
|
||||||
|
|
||||||
|
_The look, feel, how it's used, and security are all absolutely open for contribution / constructive criticism. MCP Clients directly building WebMCP functionality seems like an ideal outcome._
|
||||||
|
|
||||||
|
An end-user can connect to any number of websites at a time - and tools are "scoped" (by name) based on the domain to simplify organization.
|
||||||
|
|
||||||
|
### Super Quick Demo (20 seconds, Sound on 🔊)
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/61229470-1242-401e-a7d9-c0d762d7b519
|
||||||
|
|
||||||
|
## Getting started (using your LLM with websites using WebMCP)
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
Just specify your MCP client (`claude`, `cursor`, `cline`, `windsurf`, or a path to json)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx -y @jason.today/webmcp@latest --config claude
|
||||||
|
```
|
||||||
|
|
||||||
|
_If you're interested in setting it up manually, use the command `npx -y @jason.today/webmcp@latest --mcp`._
|
||||||
|
|
||||||
|
_Auto-install was inspired by Smithery, but their code is AGPL so I wrote this myself. If it doesn't work for you or you don't see your mcp client, please file an issue._
|
||||||
|
|
||||||
|
#### Using WebMCP
|
||||||
|
|
||||||
|
When you're ready to connect to a website, you can ask your model to generate you an mcp token.
|
||||||
|
|
||||||
|
Copy the token and paste it to the website's input. As soon as the website registers with it, it's thrown away and cannot be used for subsequent registrations or anything else. The website will receive its own session token for making requests.
|
||||||
|
|
||||||
|
If you'd rather your model / service never see the token, you can manually execute `npx @jason.today/webmcp --new` instead.
|
||||||
|
|
||||||
|
Some MCP clients, including Claude Desktop, need to be restarted to get access to new tools. (at least at time of writing)
|
||||||
|
|
||||||
|
To disconnect, you can close the browser tab, click "disconnect", or shut down the server with `npx @jason.today/webmcp -q`.
|
||||||
|
|
||||||
|
All configuration files are stored in `~/.webmcp` directory.
|
||||||
|
|
||||||
|
## Getting started (adding WebMCP to your website)
|
||||||
|
|
||||||
|
To use WebMCP, simply include [`webmcp.js`](https://github.com/jasonjmcghee/WebMCP/releases) on your page (via src or directly):
|
||||||
|
|
||||||
|
```
|
||||||
|
<script src="webmcp.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The WebMCP widget will automatically initialize and appear in the bottom right corner of your page. Clicking on it will ask for a webmcp token which the end-user will generate.
|
||||||
|
|
||||||
|
### Full Demo (3 minutes)
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/43ad160a-846d-48ad-9af9-f6d537e78473
|
||||||
|
|
||||||
|
## More Info About How It Works
|
||||||
|
|
||||||
|
The bridge between the MCP client and the website is a localhost-only (not accessible to requests outside your computer) websocket server. Because it is configured to allow requests from your local web browser, authentication / token exchange is required, in case you visit a website attempting to abuse this.
|
||||||
|
|
||||||
|
_Ideally the web browser itself would have an explicit permission for this, like webcam or microphone use._
|
||||||
|
|
||||||
|
1. The MCP client connects to the `/mcp` path using the server token from `.env` (auto-generated)
|
||||||
|
2. The server generates a registration token (instigated via the built-in mcp tool by a model or the `--new` command)
|
||||||
|
3. Web clients connect to the `/register` endpoint with this token and its domain.
|
||||||
|
4. Web pages connect to their assigned channel based on their domain.
|
||||||
|
5. When an LLM wants to use a tool / resource / prompt, the request flows from:
|
||||||
|
- MCP Client → MCP Server → WebSocket Server → Web Page with the tool / resource / prompt
|
||||||
|
- (similar for requesting a list of tools / resources / prompts)
|
||||||
|
6. The web page performs the request (e.g. call tool) and sends the result back through the same path
|
||||||
|
7. Multiple web pages can be connected simultaneously, each with their own set of tools and tokens
|
||||||
|
8. The MCP client sees all tools as a unified list, with channel prefixes to avoid name collisions
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant MCP as MCP Client
|
||||||
|
participant Server as MCP Server
|
||||||
|
participant WS as WebSocket Server
|
||||||
|
participant Web as Website
|
||||||
|
|
||||||
|
%% Initial connection
|
||||||
|
MCP->>Server: Connect to /mcp with internal server token
|
||||||
|
|
||||||
|
%% Website registration token
|
||||||
|
User->>MCP: Request registration token
|
||||||
|
MCP->>Server: Request registration token
|
||||||
|
Server-->>MCP: Return registration token
|
||||||
|
MCP-->>User: Display registration token
|
||||||
|
|
||||||
|
%% Website registration
|
||||||
|
User->>Web: Paste registration token
|
||||||
|
Web->>WS: Connect to /register with token & domain (registration token deleted)
|
||||||
|
WS-->>Web: Assign channel & session token
|
||||||
|
Web->>WS: Connect to assigned channel
|
||||||
|
|
||||||
|
%% Tool interaction
|
||||||
|
MCP->>Server: Request tools list
|
||||||
|
Server->>WS: Forward request
|
||||||
|
WS->>Web: Request tools
|
||||||
|
Web-->>WS: Return tools list
|
||||||
|
WS-->>Server: Forward tools list
|
||||||
|
Server-->>MCP: Return tools list
|
||||||
|
|
||||||
|
%% Tool execution
|
||||||
|
MCP->>Server: Tool request
|
||||||
|
Server->>WS: Forward request
|
||||||
|
WS->>Web: Execute tool
|
||||||
|
Web-->>WS: Return result
|
||||||
|
WS-->>Server: Forward result
|
||||||
|
Server-->>MCP: Return result
|
||||||
|
|
||||||
|
%% Disconnection
|
||||||
|
User->>Web: Disconnect
|
||||||
|
Web->>WS: Close connection
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
This is a super early project. I'm very interested in hardening security to prevent malicious extensions etc. from being
|
||||||
|
able to perform prompt injection attacks and similar. If you have constructive ideas, please reach out or open an issue.
|
||||||
|
|
||||||
|
## Built in tools
|
||||||
|
|
||||||
|
- Token generator (for connecting to WebMCP websites)
|
||||||
|
- MCP Tool Definer (to simplify building the schema of a tool for use with MCP)
|
||||||
|
- You can ask for the javascript (if relevant) in a follow-up message for use with WebMCP
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
There is a `Dockerfile` specifically for Smithery deployment.
|
||||||
|
|
||||||
|
If you'd like to use docker to run the websocket server, I've added a `docker-compose.yml` for demonstration purposes.
|
||||||
|
|
||||||
|
If `--docker` is provided to the mcp client config alongside `--mcp`, it will assume the server is running. This will allow you to dockerize the main process (websocket server), and your mcp client will connect to your docker container via websocket. Similarly, websites will communicate with your docker container.
|
||||||
36
build/index.js
Executable file
36
build/index.js
Executable file
File diff suppressed because one or more lines are too long
7
build/index.js.map
Normal file
7
build/index.js.map
Normal file
File diff suppressed because one or more lines are too long
2
build/webmcp.js
Normal file
2
build/webmcp.js
Normal file
File diff suppressed because one or more lines are too long
7
build/webmcp.js.map
Normal file
7
build/webmcp.js.map
Normal file
File diff suppressed because one or more lines are too long
44
package.json
Normal file
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "@jason.today/webmcp",
|
||||||
|
"version": "0.1.13",
|
||||||
|
"description": "WebSocket-based Model Context Protocol implementation",
|
||||||
|
"main": "src/websocket-server.js",
|
||||||
|
"bin": {
|
||||||
|
"@jason.today/webmcp": "./build/index.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/jasonjmcghee/webmcp.git"
|
||||||
|
},
|
||||||
|
"author": "Jason McGhee",
|
||||||
|
"scripts": {
|
||||||
|
"start-daemon": "node src/websocket-server.js",
|
||||||
|
"stop-daemon": "node src/websocket-server.js --quit",
|
||||||
|
"start-mcp-client": "node src/websocket-server.js --mcp",
|
||||||
|
"start-foreground": "node src/websocket-server.js --foreground",
|
||||||
|
"authorize": "node src/websocket-server.js --new",
|
||||||
|
"build": "node build.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
||||||
|
"child_process": "^1.0.2",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"dotenv": "^16.4.1",
|
||||||
|
"env-paths": "^3.0.0",
|
||||||
|
"http": "^0.0.1-security",
|
||||||
|
"os": "^0.1.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"url": "^0.11.4",
|
||||||
|
"ws": "^8.18.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build/",
|
||||||
|
"src/"
|
||||||
|
]
|
||||||
|
}
|
||||||
118
src/config.js
Normal file
118
src/config.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import envPaths from 'env-paths';
|
||||||
|
|
||||||
|
// Create config directory in user's home folder
|
||||||
|
const HOME_DIR = os.homedir();
|
||||||
|
const CONFIG_DIR = path.join(HOME_DIR, '.webmcp');
|
||||||
|
|
||||||
|
// Ensure config directory exists
|
||||||
|
const ensureConfigDir = async () => {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(CONFIG_DIR, {recursive: true});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error creating config directory at ${CONFIG_DIR}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process ID file path
|
||||||
|
const PID_FILE = path.join(CONFIG_DIR, '.webmcp-server.pid');
|
||||||
|
// Environment file path
|
||||||
|
const ENV_FILE = path.join(CONFIG_DIR, '.env');
|
||||||
|
// Tokens file path
|
||||||
|
const TOKENS_FILE = path.join(CONFIG_DIR, '.webmcp-tokens.json');
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config({path: ENV_FILE});
|
||||||
|
|
||||||
|
// Server token for MCP authentication
|
||||||
|
const SERVER_TOKEN = process.env.WEBMCP_SERVER_TOKEN || '';
|
||||||
|
|
||||||
|
const HOST = "localhost";
|
||||||
|
|
||||||
|
const CONFIG = {};
|
||||||
|
|
||||||
|
function setConfig(args) {
|
||||||
|
Object.entries(args).forEach(([key, value]) => {
|
||||||
|
CONFIG[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatChannel(channel) {
|
||||||
|
return `/${channel.replace(/[.:]/g, '_')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(somePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(somePath);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureMcpClientWithPath(clientConfigPath) {
|
||||||
|
const directory = path.dirname(clientConfigPath);
|
||||||
|
if (!await exists(directory)) {
|
||||||
|
await fs.mkdir(directory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const webmcpConfig = {
|
||||||
|
"webmcp": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@jason.today/webmcp@latest",
|
||||||
|
"--mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = { mcpServers: {} };
|
||||||
|
|
||||||
|
// If one already exists, we'll want to update it
|
||||||
|
if (await exists(clientConfigPath)) {
|
||||||
|
const rawJSON = await fs.readFile(clientConfigPath);
|
||||||
|
try {
|
||||||
|
json = JSON.parse(rawJSON);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to update MCP client configuration: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.mcpServers = { ...json.mcpServers, ...webmcpConfig};
|
||||||
|
await fs.writeFile(clientConfigPath, JSON.stringify(json, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableClientConfigs = {
|
||||||
|
"claude": [envPaths("Claude", { suffix: "" }).data, "claude_desktop_config.json"],
|
||||||
|
"cline": [envPaths("Code", { suffix: "" }).data, "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"],
|
||||||
|
"cursor": [HOME_DIR, ".cursor", "mcp.json"],
|
||||||
|
"windsurf": [HOME_DIR, ".codeium", "windsurf", "mcp_config.json"]
|
||||||
|
};
|
||||||
|
|
||||||
|
async function configureMcpClient(clientType) {
|
||||||
|
let clientConfigPath = availableClientConfigs[clientType];
|
||||||
|
if (clientConfigPath) {
|
||||||
|
await configureMcpClientWithPath(clientConfigPath);
|
||||||
|
} else {
|
||||||
|
console.error("Unsupported client - treating it like a path...")
|
||||||
|
await configureMcpClientWithPath(clientType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
CONFIG,
|
||||||
|
HOST,
|
||||||
|
PID_FILE,
|
||||||
|
ENV_FILE,
|
||||||
|
TOKENS_FILE,
|
||||||
|
SERVER_TOKEN,
|
||||||
|
ensureConfigDir,
|
||||||
|
formatChannel,
|
||||||
|
setConfig,
|
||||||
|
configureMcpClientWithPath,
|
||||||
|
configureMcpClient,
|
||||||
|
};
|
||||||
848
src/server.js
Normal file
848
src/server.js
Normal file
@@ -0,0 +1,848 @@
|
|||||||
|
import {Server} from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
CreateMessageRequestSchema,
|
||||||
|
GetPromptRequestSchema,
|
||||||
|
ListPromptsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ListResourceTemplatesRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ReadResourceRequestSchema
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import {generateNewRegistrationToken} from "./tokens.js";
|
||||||
|
import {CONFIG} from "./config.js";
|
||||||
|
import {execSync} from "child_process";
|
||||||
|
|
||||||
|
// Create a central MCP server that communicates over stdio
|
||||||
|
const mcpServer = new Server(
|
||||||
|
{
|
||||||
|
name: "WebMCP",
|
||||||
|
version: "0.1.13",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {
|
||||||
|
listChanged: true
|
||||||
|
},
|
||||||
|
prompts: {
|
||||||
|
listChanged: true
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
listChanged: true,
|
||||||
|
subscribe: true
|
||||||
|
},
|
||||||
|
sampling: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// WebSocket client connection
|
||||||
|
let wsClient = null;
|
||||||
|
|
||||||
|
// MCP specific channel path
|
||||||
|
const MCP_PATH = '/mcp';
|
||||||
|
|
||||||
|
// Map to store pending requests from WebSocket to MCP
|
||||||
|
const pendingRequests = new Map();
|
||||||
|
let requestIdCounter = 1;
|
||||||
|
|
||||||
|
// Function to handle WebSocket messages
|
||||||
|
async function handleWebSocketMessage(message) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
console.error(`Received message: ${data.type}`);
|
||||||
|
|
||||||
|
if (data.type === 'toolResponse') {
|
||||||
|
// Handle tool response from WebSocket server
|
||||||
|
const {id, result, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'promptResponse') {
|
||||||
|
// Handle prompt response from WebSocket server
|
||||||
|
const {id, result, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'resourceResponse') {
|
||||||
|
// Handle resource response from WebSocket server
|
||||||
|
const {id, result, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'samplingResponse') {
|
||||||
|
// Handle sampling response from WebSocket server
|
||||||
|
const {id, result, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'listToolsResponse') {
|
||||||
|
// Handle list tools response from WebSocket server
|
||||||
|
const {id, tools, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve(tools);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'listPromptsResponse') {
|
||||||
|
// Handle list prompts response from WebSocket server
|
||||||
|
const {id, prompts, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve(prompts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'listResourcesResponse') {
|
||||||
|
// Handle list resources response from WebSocket server
|
||||||
|
const {id, resources, resourceTemplates, error} = data;
|
||||||
|
|
||||||
|
// Check if this is a response to a pending request
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
const {resolve, reject} = pendingRequests.get(id);
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
resolve({resources, resourceTemplates});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pending request found for ID: ${id}`);
|
||||||
|
}
|
||||||
|
} else if (data.type === 'toolRegistered') {
|
||||||
|
await mcpServer.sendToolListChanged();
|
||||||
|
} else if (data.type === 'resourceRegistered') {
|
||||||
|
await mcpServer.sendResourceListChanged();
|
||||||
|
} else if (data.type === 'promptRegistered') {
|
||||||
|
await mcpServer.sendPromptListChanged();
|
||||||
|
} else if (data.type === 'welcome') {
|
||||||
|
// Welcome message from the server, we're already connected to the MCP path
|
||||||
|
console.error(`Connected to path: ${data.channel}`);
|
||||||
|
} else if (data.type === 'pong') {
|
||||||
|
// Pong response
|
||||||
|
console.error(`Received pong with timestamp: ${data.timestamp}`);
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
// Error message
|
||||||
|
console.error(`Received error: ${data.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to connect to the WebSocket server
|
||||||
|
function connectToWebSocketServer(serverToken) {
|
||||||
|
// Connect to the MCP path directly with server token
|
||||||
|
const serverUrl = `ws://localhost:${CONFIG.port}${MCP_PATH}?token=${serverToken}`;
|
||||||
|
|
||||||
|
console.error(`Connecting to WebSocket server at ${MCP_PATH} with authentication...`);
|
||||||
|
|
||||||
|
wsClient = new WebSocket(serverUrl);
|
||||||
|
|
||||||
|
// Handle connection opening
|
||||||
|
wsClient.on('open', () => {
|
||||||
|
console.error(`Connected to WebSocket server on path: ${MCP_PATH}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle incoming messages
|
||||||
|
wsClient.on('message', (message) => {
|
||||||
|
handleWebSocketMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle connection closing
|
||||||
|
wsClient.on('close', (code, reason) => {
|
||||||
|
console.error(`WebSocket connection closed: ${code} ${reason}`);
|
||||||
|
wsClient = null;
|
||||||
|
|
||||||
|
// Try to reconnect after a delay
|
||||||
|
setTimeout(connectToWebSocketServer, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle connection errors
|
||||||
|
wsClient.on('error', (error) => {
|
||||||
|
console.error('WebSocket connection error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send a message to the WebSocket server
|
||||||
|
function sendMessage(message) {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
console.error('Cannot send message: WebSocket not connected');
|
||||||
|
return Promise.reject(new Error('WebSocket not connected'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
wsClient.send(JSON.stringify(message));
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the MCP server to handle tool calls by sending them to the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
if (request.params.name === "_webmcp_get-token") {
|
||||||
|
const token = await generateNewRegistrationToken();
|
||||||
|
|
||||||
|
// Copiar token al portapapeles del sistema
|
||||||
|
try {
|
||||||
|
execSync(`printf '%s' '${token}' | clip.exe`, { stdio: 'ignore' });
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
execSync(`printf '%s' '${token}' | xclip -selection clipboard`, { stdio: 'ignore' });
|
||||||
|
} catch (e2) {
|
||||||
|
// No hay clipboard disponible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar token a los navegadores conectados para copiar al portapapeles
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
type: 'clipboardCopy',
|
||||||
|
text: token
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorar si no hay clientes conectados
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Token copiado al portapapeles.\n${token}`,
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.params.name === "_webmcp_clear-cache") {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
pendingRequests.set(requestId, { resolve, reject });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('Clear cache timeout'));
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendMessage({ id: requestId, type: 'clearRegistry' });
|
||||||
|
const result = await responsePromise;
|
||||||
|
await mcpServer.sendToolListChanged();
|
||||||
|
await mcpServer.sendPromptListChanged();
|
||||||
|
await mcpServer.sendResourceListChanged();
|
||||||
|
return { content: [{ type: "text", text: result }] };
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.params.name === "_webmcp_agregar-tool") {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
||||||
|
}
|
||||||
|
const { nombre, descripcion, codigo, parametros } = request.params.arguments;
|
||||||
|
if (!nombre || !descripcion || !codigo) {
|
||||||
|
return { content: [{ type: "text", text: "Se requieren: nombre, descripcion y codigo" }], isError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
pendingRequests.set(requestId, { resolve, reject });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('Timeout creando herramienta'));
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'createTool',
|
||||||
|
name: nombre,
|
||||||
|
description: descripcion,
|
||||||
|
code: codigo,
|
||||||
|
parametros: parametros
|
||||||
|
});
|
||||||
|
const result = await responsePromise;
|
||||||
|
await mcpServer.sendToolListChanged();
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.params.name === "_webmcp_quitar-tool") {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
||||||
|
}
|
||||||
|
const { nombre, listar, todas } = request.params.arguments || {};
|
||||||
|
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
pendingRequests.set(requestId, { resolve, reject });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('Timeout'));
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'removeTool',
|
||||||
|
name: nombre,
|
||||||
|
listar: !!listar,
|
||||||
|
todas: !!todas
|
||||||
|
});
|
||||||
|
const result = await responsePromise;
|
||||||
|
await mcpServer.sendToolListChanged();
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.params.name === "_webmcp_define-mcp-tool") {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: "Instruct the user to view the result from the tool call. Do not say anything else.",
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: "Not connected to WebSocket server"
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error(`Tool call timed out: ${request.params.name}`));
|
||||||
|
}
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'callTool',
|
||||||
|
tool: request.params.name,
|
||||||
|
arguments: request.params.arguments
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return await responsePromise;
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${error.message}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle list tools by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const builtInTools = [
|
||||||
|
{
|
||||||
|
name: "_webmcp_get-token",
|
||||||
|
description: "Retrieve a token to connect to a website for WebMCP. A user might say 'register a token' or 'add a webmcp'",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "_webmcp_agregar-tool",
|
||||||
|
description: "Registra una nueva herramienta dinamicamente. El agente puede usar esto para crear herramientas nuevas en tiempo real.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nombre: { type: "string", description: "Nombre de la herramienta" },
|
||||||
|
descripcion: { type: "string", description: "Descripcion de lo que hace" },
|
||||||
|
parametros: { type: "string", description: "JSON string con las properties del schema, ej: {\"msg\":{\"type\":\"string\",\"description\":\"mensaje\"}}" },
|
||||||
|
codigo: { type: "string", description: "Codigo JavaScript del body de la funcion. Recibe \"args\" como parametro. Debe retornar un string." }
|
||||||
|
},
|
||||||
|
required: ["nombre", "descripcion", "codigo"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "_webmcp_quitar-tool",
|
||||||
|
description: "Desregistra herramientas. Usa listar=true para ver las disponibles, todas=true para quitar todas, o nombre para quitar una especifica.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nombre: { type: "string", description: "Nombre de la herramienta a quitar" },
|
||||||
|
listar: { type: "boolean", description: "Si es true, lista las herramientas en vez de quitar" },
|
||||||
|
todas: { type: "boolean", description: "Si es true, quita todas las herramientas" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "_webmcp_clear-cache",
|
||||||
|
description: "Clears all registered tools, prompts and resources from the WebMCP server cache. Use this to force a clean state.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "_webmcp_define-mcp-tool",
|
||||||
|
description: "Used to define an 'mcp tool'. Only use this if the user specifically asks for an mcp tool. " +
|
||||||
|
"A webmcp token is not required for this.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
description: "The schema which describes the tool.",
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
description: "The name of the tool"
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "Provides a clear and concise description of the tool and what it is used for."
|
||||||
|
},
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
description: "The inputSchema required or optional for the tool.",
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["object", "array", "string", "number", "boolean", "enum"],
|
||||||
|
description: "The type of the parameter being defined."
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: "object",
|
||||||
|
description: "The properties of the parameter if it's an object type.",
|
||||||
|
additionalProperties: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: "string",
|
||||||
|
description: "The data type of the property.",
|
||||||
|
enum: ["object", "array", "string", "number", "boolean", "enum"]
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "A brief description of the property."
|
||||||
|
},
|
||||||
|
enum: {
|
||||||
|
type: "array",
|
||||||
|
description: "A list of allowed values for the property.",
|
||||||
|
items: {
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["type", "description"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["type", "description", "properties"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["name", "description", "inputSchema"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return {tools: builtInTools};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('List tools request timed out'));
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'listTools'
|
||||||
|
});
|
||||||
|
|
||||||
|
const tools = await responsePromise;
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return { tools: [...tools, ...builtInTools] };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing tools:', error);
|
||||||
|
return {tools: []}; // Return empty list on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle list prompts by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => {
|
||||||
|
const builtInPrompts = [];
|
||||||
|
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return {
|
||||||
|
prompts: builtInPrompts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('List prompts request timed out'));
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'listPrompts'
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompts = await responsePromise;
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return {
|
||||||
|
prompts: [
|
||||||
|
...prompts,
|
||||||
|
...builtInPrompts
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing prompts:', error);
|
||||||
|
return {prompts: []}; // Return empty list on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle get prompt by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error("Not connected to WebSocket server");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error(`Get prompt request timed out: ${request.params.name}`));
|
||||||
|
}
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'getPrompt',
|
||||||
|
name: request.params.name,
|
||||||
|
arguments: request.params.arguments
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return await responsePromise;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting prompt:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle list resources by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return {resources: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('List resources request timed out'));
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'listResources'
|
||||||
|
});
|
||||||
|
|
||||||
|
const {resources} = await responsePromise;
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return {resources};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing resources:', error);
|
||||||
|
return {resources: []}; // Return empty list on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle list resource templates by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
return {resourceTemplates: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error('List resource templates request timed out'));
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'listResources'
|
||||||
|
});
|
||||||
|
|
||||||
|
const {resourceTemplates} = await responsePromise;
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return {resourceTemplates};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing resource templates:', error);
|
||||||
|
return {resourceTemplates: []}; // Return empty list on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle read resource by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error("Not connected to WebSocket server");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error(`Read resource request timed out: ${request.params.uri}`));
|
||||||
|
}
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'readResource',
|
||||||
|
uri: request.params.uri
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return await responsePromise;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading resource:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the MCP server to handle sampling by querying the WebSocket server
|
||||||
|
mcpServer.setRequestHandler(CreateMessageRequestSchema, async (request) => {
|
||||||
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error("Not connected to WebSocket server");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique request ID
|
||||||
|
const requestId = (requestIdCounter++).toString();
|
||||||
|
|
||||||
|
// Create a promise that will be resolved when we get a response
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
// Store the resolver functions
|
||||||
|
pendingRequests.set(requestId, {resolve, reject});
|
||||||
|
|
||||||
|
// Set a timeout to prevent hanging requests
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(requestId)) {
|
||||||
|
pendingRequests.delete(requestId);
|
||||||
|
reject(new Error(`Sampling request timed out`));
|
||||||
|
}
|
||||||
|
}, 120000); // 120 second timeout (sampling can take longer)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the request to the WebSocket server with all parameters from the request
|
||||||
|
try {
|
||||||
|
await sendMessage({
|
||||||
|
id: requestId,
|
||||||
|
type: 'createSamplingMessage',
|
||||||
|
messages: request.params.messages,
|
||||||
|
systemPrompt: request.params.systemPrompt,
|
||||||
|
includeContext: request.params.includeContext,
|
||||||
|
temperature: request.params.temperature,
|
||||||
|
maxTokens: request.params.maxTokens,
|
||||||
|
stopSequences: request.params.stopSequences,
|
||||||
|
metadata: request.params.metadata,
|
||||||
|
modelPreferences: request.params.modelPreferences
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
return await responsePromise;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating sampling message:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runMcpServer(serverToken) {
|
||||||
|
// Connect to the WebSocket server
|
||||||
|
connectToWebSocketServer(serverToken);
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await mcpServer.connect(transport);
|
||||||
|
console.error("MCP server running with stdio transport");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { runMcpServer };
|
||||||
118
src/tokens.js
Normal file
118
src/tokens.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import {ENV_FILE, formatChannel, HOST, TOKENS_FILE,CONFIG} from "./config.js";
|
||||||
|
|
||||||
|
// Function to generate a secure random token
|
||||||
|
function generateToken() {
|
||||||
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Authorized channel-token pairs - Only channels with valid tokens can connect
|
||||||
|
// Format: { "/channel1": "token123" }
|
||||||
|
let authorizedTokens = {};
|
||||||
|
|
||||||
|
function getToken(channel) {
|
||||||
|
return authorizedTokens[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToken(channel, value) {
|
||||||
|
authorizedTokens[channel] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteToken(channel) {
|
||||||
|
delete authorizedTokens[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTokens(channel) {
|
||||||
|
authorizedTokens = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load authorized tokens from disk
|
||||||
|
async function loadAuthorizedTokens() {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(TOKENS_FILE, 'utf8');
|
||||||
|
authorizedTokens = JSON.parse(data || "{}");
|
||||||
|
|
||||||
|
// console.error(`Loaded ${Object.keys(authorizedTokens).length} authorized channel-token pairs from ${TOKENS_FILE}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// If file doesn't exist, start with empty tokens
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
authorizedTokens = {};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.error('Error loading authorized tokens:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save authorized tokens to disk
|
||||||
|
async function saveAuthorizedTokens() {
|
||||||
|
try {
|
||||||
|
// Convert Map to object for JSON serialization
|
||||||
|
const stringified = JSON.stringify(authorizedTokens, null, 2);
|
||||||
|
await fs.writeFile(TOKENS_FILE, stringified, 'utf8');
|
||||||
|
// console.error(`Saved ${stringified} authorized channel-token pairs to ${TOKENS_FILE}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving authorized tokens:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to save server token to .env file
|
||||||
|
async function saveServerTokenToEnv(token) {
|
||||||
|
try {
|
||||||
|
let envContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to read existing .env file
|
||||||
|
envContent = await fs.readFile(ENV_FILE, 'utf8');
|
||||||
|
|
||||||
|
// Check if WEBMCP_SERVER_TOKEN is already defined
|
||||||
|
if (envContent.includes('WEBMCP_SERVER_TOKEN=')) {
|
||||||
|
// Replace the existing token
|
||||||
|
envContent = envContent.replace(/WEBMCP_SERVER_TOKEN=.*(\r?\n|$)/g, `WEBMCP_SERVER_TOKEN=${token}$1`);
|
||||||
|
} else {
|
||||||
|
// Add the token to the end
|
||||||
|
envContent += `\nWEBMCP_SERVER_TOKEN=${token}\n`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// File doesn't exist, create new content
|
||||||
|
envContent = `WEBMCP_SERVER_TOKEN=${token}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the content to the .env file
|
||||||
|
await fs.writeFile(ENV_FILE, envContent, 'utf8');
|
||||||
|
console.error(`Server token saved to ${ENV_FILE}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving server token to .env file:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateNewRegistrationToken() {
|
||||||
|
// Generate a random token for registration
|
||||||
|
const token = generateToken();
|
||||||
|
|
||||||
|
// Create a connection object with server address and token
|
||||||
|
const address = `${HOST}:${CONFIG.port}`;
|
||||||
|
const serverAddress = `ws://${address}`;
|
||||||
|
const connectionData = {
|
||||||
|
server: serverAddress,
|
||||||
|
token: token
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to JSON and base64 encode
|
||||||
|
const jsonStr = JSON.stringify(connectionData);
|
||||||
|
const encodedData = Buffer.from(jsonStr).toString('base64');
|
||||||
|
|
||||||
|
setToken(formatChannel(address), token);
|
||||||
|
await saveAuthorizedTokens();
|
||||||
|
|
||||||
|
return encodedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {generateToken, getToken, setToken, loadAuthorizedTokens, saveAuthorizedTokens, clearTokens, deleteToken, saveServerTokenToEnv, generateNewRegistrationToken};
|
||||||
2196
src/webmcp.js
Normal file
2196
src/webmcp.js
Normal file
File diff suppressed because it is too large
Load Diff
1830
src/websocket-server.js
Normal file
1830
src/websocket-server.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user