@remcp/remcp 0.1.3 → 0.2.4
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 +41 -113
- package/package.json +10 -24
- package/src/agent.mjs +215 -36
- package/src/cli.mjs +217 -43
- package/src/runtime.mjs +15 -0
- package/src/version.mjs +1 -3
- package/THIRD_PARTY_NOTICES.md +0 -7
- package/assets/remcp-icon.png +0 -0
- package/mcp.json +0 -9
- package/plugin.json +0 -37
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/app.js +0 -96
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/art/hero-relay.webp +0 -0
- package/public/assets/art/relay-detail.webp +0 -0
- package/public/assets/icon-120.png +0 -0
- package/public/assets/icon-16.png +0 -0
- package/public/assets/icon-180.png +0 -0
- package/public/assets/icon-192.png +0 -0
- package/public/assets/icon-256.png +0 -0
- package/public/assets/icon-32.png +0 -0
- package/public/assets/icon-48.png +0 -0
- package/public/assets/icon-512.png +0 -0
- package/public/assets/og-remcp.png +0 -0
- package/public/authorize.html +0 -59
- package/public/copy.js +0 -40
- package/public/docs.html +0 -93
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +0 -134
- package/public/oauth-app.js +0 -63
- package/public/privacy.html +0 -80
- package/public/remcp-darkmode-16x16.png +0 -0
- package/public/remcp-darkmode-192x192.png +0 -0
- package/public/remcp-darkmode-32x32.png +0 -0
- package/public/remcp-darkmode-48x48.png +0 -0
- package/public/remcp-darkmode-512x512.png +0 -0
- package/public/security.html +0 -70
- package/public/site.webmanifest +0 -23
- package/public/style.css +0 -261
- package/public/support.html +0 -75
- package/public/terms.html +0 -77
- package/skills/remcp-operator/SKILL.md +0 -36
- package/src/auth.mjs +0 -71
- package/src/config.mjs +0 -51
- package/src/db.mjs +0 -102
- package/src/mcp.mjs +0 -95
- package/src/oauth.mjs +0 -164
- package/src/relay.mjs +0 -97
- package/src/review-sandbox.mjs +0 -96
- package/src/server.mjs +0 -159
- package/src/tool-catalog.json +0 -796
- package/src/util.mjs +0 -24
package/src/server.mjs
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import express from 'express';
|
|
5
|
-
import helmet from 'helmet';
|
|
6
|
-
import { rateLimit } from 'express-rate-limit';
|
|
7
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
|
-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import { config } from './config.mjs';
|
|
10
|
-
import { createDevice, createPairCode, consumePairCode, revokeDevice } from './db.mjs';
|
|
11
|
-
import { requireFirebase, requireMcpAuth } from './auth.mjs';
|
|
12
|
-
import { oauthRouter } from './oauth.mjs';
|
|
13
|
-
import { createRelay } from './relay.mjs';
|
|
14
|
-
import { createMcpServer } from './mcp.mjs';
|
|
15
|
-
import { now, randomId, randomToken, sha256 } from './util.mjs';
|
|
16
|
-
import { VERSION } from './version.mjs';
|
|
17
|
-
|
|
18
|
-
const app = express();
|
|
19
|
-
app.set('trust proxy', 1);
|
|
20
|
-
app.disable('x-powered-by');
|
|
21
|
-
const firebaseFrame = `https://${config.firebase.authDomain}`;
|
|
22
|
-
app.use(helmet({
|
|
23
|
-
contentSecurityPolicy: {
|
|
24
|
-
directives: {
|
|
25
|
-
defaultSrc: ["'self'"],
|
|
26
|
-
scriptSrc: ["'self'", 'https://www.gstatic.com', 'https://apis.google.com', 'https://www.google.com'],
|
|
27
|
-
connectSrc: ["'self'", 'https://identitytoolkit.googleapis.com', 'https://securetoken.googleapis.com', 'https://www.googleapis.com', 'https://apis.google.com', 'https://accounts.google.com'],
|
|
28
|
-
imgSrc: ["'self'", 'data:'],
|
|
29
|
-
styleSrc: ["'self'"],
|
|
30
|
-
fontSrc: ["'none'"],
|
|
31
|
-
frameSrc: [firebaseFrame, 'https://accounts.google.com', 'https://apis.google.com'],
|
|
32
|
-
objectSrc: ["'none'"],
|
|
33
|
-
baseUri: ["'none'"],
|
|
34
|
-
formAction: ["'self'", 'https://accounts.google.com'],
|
|
35
|
-
frameAncestors: ["'none'"],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
|
|
39
|
-
strictTransportSecurity: config.publicUrl.startsWith('https://') ? { maxAge: 31536000, includeSubDomains: false } : false,
|
|
40
|
-
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
41
|
-
}));
|
|
42
|
-
app.use((req, res, next) => {
|
|
43
|
-
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
44
|
-
const dynamicPath = req.path === '/health' || req.path === '/mcp' || req.path.startsWith('/api/') || req.path.startsWith('/oauth/') || req.path.startsWith('/.well-known/');
|
|
45
|
-
if (dynamicPath) {
|
|
46
|
-
res.setHeader('Cache-Control', 'no-store, max-age=0');
|
|
47
|
-
res.setHeader('CDN-Cache-Control', 'no-store');
|
|
48
|
-
res.setHeader('Cloudflare-CDN-Cache-Control', 'no-store');
|
|
49
|
-
}
|
|
50
|
-
next();
|
|
51
|
-
});
|
|
52
|
-
app.use(express.json({ limit: '256kb' }));
|
|
53
|
-
app.use(express.urlencoded({ extended: false, limit: '256kb', parameterLimit: 50 }));
|
|
54
|
-
const pairClaimLimiter = rateLimit({ windowMs: 60_000, limit: 20, standardHeaders: 'draft-8', legacyHeaders: false });
|
|
55
|
-
|
|
56
|
-
const httpServer = http.createServer(app);
|
|
57
|
-
const relay = createRelay(httpServer);
|
|
58
|
-
const sessions = new Map();
|
|
59
|
-
const publicDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../public');
|
|
60
|
-
|
|
61
|
-
app.get('/health', (_req, res) => res.json({ ok: true, service: 'remcp', version: VERSION, connectedDevices: relay.onlineCount() }));
|
|
62
|
-
app.get(config.domainVerificationPath, (_req, res) => {
|
|
63
|
-
if (!config.domainVerificationToken) return res.status(404).type('text/plain').send('Not configured');
|
|
64
|
-
res.type('text/plain').send(config.domainVerificationToken);
|
|
65
|
-
});
|
|
66
|
-
app.get('/api/config', (_req, res) => res.json({ publicUrl: config.publicUrl, mcpUrl: `${config.publicUrl}/mcp`, firebase: config.firebase }));
|
|
67
|
-
app.get('/.well-known/security.txt', (_req, res) => res.type('text/plain').send(`Contact: ${config.publicUrl}/support\nPolicy: ${config.publicUrl}/security\nExpires: 2027-09-16T00:00:00Z\nCanonical: ${config.publicUrl}/.well-known/security.txt\n`));
|
|
68
|
-
|
|
69
|
-
app.post('/api/pair/create', requireFirebase, (req, res) => {
|
|
70
|
-
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
71
|
-
let code = '';
|
|
72
|
-
const bytes = Buffer.from(randomToken(12));
|
|
73
|
-
for (let i = 0; i < 8; i++) code += alphabet[bytes[i] % alphabet.length];
|
|
74
|
-
const expiresAt = now() + config.pairTtlSeconds * 1000;
|
|
75
|
-
createPairCode(req.user.uid, sha256(code), expiresAt);
|
|
76
|
-
res.json({ code, expiresAt, command: `npx --yes ${config.installSpec} connect --server ${config.publicUrl} --code ${code} --install` });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
app.post('/api/pair/claim', pairClaimLimiter, (req, res) => {
|
|
80
|
-
const code = String(req.body?.code || '').replace(/\s+/g, '').toUpperCase();
|
|
81
|
-
const pair = consumePairCode(sha256(code));
|
|
82
|
-
if (!pair) return res.status(400).json({ error: 'invalid_or_expired_pairing_code' });
|
|
83
|
-
const deviceToken = randomToken(32);
|
|
84
|
-
const device = createDevice({
|
|
85
|
-
id: randomId(), uid: pair.uid,
|
|
86
|
-
name: String(req.body?.name || req.body?.hostname || 'My computer').slice(0, 100),
|
|
87
|
-
tokenHash: sha256(deviceToken), hostname: String(req.body?.hostname || '').slice(0, 180),
|
|
88
|
-
platform: String(req.body?.platform || '').slice(0, 32), arch: String(req.body?.arch || '').slice(0, 32),
|
|
89
|
-
});
|
|
90
|
-
res.json({ deviceId: device.id, deviceToken, agentUrl: config.publicUrl.replace(/^http/, 'ws') + '/agent' });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
app.get('/api/me', requireFirebase, (req, res) => res.json({ user: req.user, devices: relay.devicesFor(req.user.uid) }));
|
|
94
|
-
app.delete('/api/devices/:id', requireFirebase, (req, res) => {
|
|
95
|
-
const result = revokeDevice(req.user.uid, String(req.params.id));
|
|
96
|
-
if (!result.changes) return res.status(404).json({ error: 'device_not_found' });
|
|
97
|
-
res.status(204).end();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
app.use(oauthRouter);
|
|
101
|
-
app.all('/mcp', requireMcpAuth, async (req, res) => {
|
|
102
|
-
try {
|
|
103
|
-
const sessionId = req.headers['mcp-session-id'];
|
|
104
|
-
let entry = typeof sessionId === 'string' ? sessions.get(sessionId) : undefined;
|
|
105
|
-
if (entry && entry.uid !== req.mcpAuth.uid) return res.status(403).json({ error: 'session_owner_mismatch' });
|
|
106
|
-
if (!entry) {
|
|
107
|
-
if (sessionId || !isInitializeRequest(req.body)) return res.status(400).json({ error: 'invalid_or_missing_mcp_session' });
|
|
108
|
-
const mcpServer = createMcpServer(req.mcpAuth.uid, relay);
|
|
109
|
-
let transport;
|
|
110
|
-
transport = new StreamableHTTPServerTransport({
|
|
111
|
-
sessionIdGenerator: randomId,
|
|
112
|
-
onsessioninitialized: id => sessions.set(id, { transport, mcpServer, uid: req.mcpAuth.uid }),
|
|
113
|
-
onsessionclosed: id => sessions.delete(id),
|
|
114
|
-
});
|
|
115
|
-
transport.onclose = () => {
|
|
116
|
-
const id = transport.sessionId;
|
|
117
|
-
if (id) sessions.delete(id);
|
|
118
|
-
void mcpServer.close();
|
|
119
|
-
};
|
|
120
|
-
await mcpServer.connect(transport);
|
|
121
|
-
entry = { transport, mcpServer, uid: req.mcpAuth.uid };
|
|
122
|
-
}
|
|
123
|
-
await entry.transport.handleRequest(req, res, req.body);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.error('MCP request failed', error);
|
|
126
|
-
if (!res.headersSent) res.status(500).json({ error: 'mcp_request_failed' });
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const noStoreExtensions = new Set(['.html', '.css', '.js', '.webmanifest']);
|
|
131
|
-
app.use(express.static(publicDir, {
|
|
132
|
-
extensions: ['html'],
|
|
133
|
-
maxAge: 0,
|
|
134
|
-
dotfiles: 'deny',
|
|
135
|
-
setHeaders: (res, filePath) => {
|
|
136
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
137
|
-
if (noStoreExtensions.has(extension)) {
|
|
138
|
-
// The public site changes independently from the API. Never let a CDN
|
|
139
|
-
// combine HTML from one deploy with CSS/JS from another deploy.
|
|
140
|
-
res.setHeader('Cache-Control', 'no-store, max-age=0');
|
|
141
|
-
res.setHeader('CDN-Cache-Control', 'no-store');
|
|
142
|
-
res.setHeader('Cloudflare-CDN-Cache-Control', 'no-store');
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
146
|
-
},
|
|
147
|
-
}));
|
|
148
|
-
app.get('*path', (req, res) => {
|
|
149
|
-
if (!req.accepts('html')) return res.status(404).json({ error: 'not_found' });
|
|
150
|
-
res.status(404).sendFile(path.join(publicDir, 'index.html'));
|
|
151
|
-
});
|
|
152
|
-
app.use((error, _req, res, _next) => {
|
|
153
|
-
console.error('Unhandled request error', error instanceof Error ? error.message : String(error));
|
|
154
|
-
if (!res.headersSent) res.status(500).json({ error: 'internal_server_error' });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
httpServer.listen(config.port, '0.0.0.0', () => {
|
|
158
|
-
console.log(`ReMCP ${config.publicUrl} listening on 0.0.0.0:${config.port}`);
|
|
159
|
-
});
|