@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.
Files changed (56) hide show
  1. package/README.md +41 -113
  2. package/package.json +10 -24
  3. package/src/agent.mjs +215 -36
  4. package/src/cli.mjs +217 -43
  5. package/src/runtime.mjs +15 -0
  6. package/src/version.mjs +1 -3
  7. package/THIRD_PARTY_NOTICES.md +0 -7
  8. package/assets/remcp-icon.png +0 -0
  9. package/mcp.json +0 -9
  10. package/plugin.json +0 -37
  11. package/public/android-chrome-192x192.png +0 -0
  12. package/public/android-chrome-512x512.png +0 -0
  13. package/public/app.js +0 -96
  14. package/public/apple-touch-icon.png +0 -0
  15. package/public/assets/art/hero-relay.webp +0 -0
  16. package/public/assets/art/relay-detail.webp +0 -0
  17. package/public/assets/icon-120.png +0 -0
  18. package/public/assets/icon-16.png +0 -0
  19. package/public/assets/icon-180.png +0 -0
  20. package/public/assets/icon-192.png +0 -0
  21. package/public/assets/icon-256.png +0 -0
  22. package/public/assets/icon-32.png +0 -0
  23. package/public/assets/icon-48.png +0 -0
  24. package/public/assets/icon-512.png +0 -0
  25. package/public/assets/og-remcp.png +0 -0
  26. package/public/authorize.html +0 -59
  27. package/public/copy.js +0 -40
  28. package/public/docs.html +0 -93
  29. package/public/favicon-16x16.png +0 -0
  30. package/public/favicon-32x32.png +0 -0
  31. package/public/favicon-48x48.png +0 -0
  32. package/public/favicon.ico +0 -0
  33. package/public/index.html +0 -134
  34. package/public/oauth-app.js +0 -63
  35. package/public/privacy.html +0 -80
  36. package/public/remcp-darkmode-16x16.png +0 -0
  37. package/public/remcp-darkmode-192x192.png +0 -0
  38. package/public/remcp-darkmode-32x32.png +0 -0
  39. package/public/remcp-darkmode-48x48.png +0 -0
  40. package/public/remcp-darkmode-512x512.png +0 -0
  41. package/public/security.html +0 -70
  42. package/public/site.webmanifest +0 -23
  43. package/public/style.css +0 -261
  44. package/public/support.html +0 -75
  45. package/public/terms.html +0 -77
  46. package/skills/remcp-operator/SKILL.md +0 -36
  47. package/src/auth.mjs +0 -71
  48. package/src/config.mjs +0 -51
  49. package/src/db.mjs +0 -102
  50. package/src/mcp.mjs +0 -95
  51. package/src/oauth.mjs +0 -164
  52. package/src/relay.mjs +0 -97
  53. package/src/review-sandbox.mjs +0 -96
  54. package/src/server.mjs +0 -159
  55. package/src/tool-catalog.json +0 -796
  56. 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
- });