Fork de @jason.today/webmcp v0.1.13 con parches Nucleo: registro dinamico, clipboard y clear-cache

This commit is contained in:
2026-02-12 22:37:47 -06:00
commit ca5cf0e3b0
12 changed files with 5368 additions and 0 deletions

21
LICENSE Normal file
View 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
View File

@@ -0,0 +1,141 @@
# WebMCP
A proposal and code for websites to support client side LLMs
[![NPM Version](https://img.shields.io/npm/v/%40jason.today%2Fwebmcp)](https://www.npmjs.com/package/@jason.today/webmcp) [![MIT licensed](https://img.shields.io/npm/l/%40jason.today%2Fwebmcp)](./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

File diff suppressed because one or more lines are too long

7
build/index.js.map Normal file

File diff suppressed because one or more lines are too long

2
build/webmcp.js Normal file

File diff suppressed because one or more lines are too long

7
build/webmcp.js.map Normal file

File diff suppressed because one or more lines are too long

44
package.json Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1830
src/websocket-server.js Normal file

File diff suppressed because it is too large Load Diff