@remcp/remcp 0.1.0

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/THIRD_PARTY_NOTICES.md +7 -0
  4. package/bin/remcp.mjs +3 -0
  5. package/chatgpt-app-submission.json +356 -0
  6. package/package.json +63 -0
  7. package/public/android-chrome-192x192.png +0 -0
  8. package/public/android-chrome-512x512.png +0 -0
  9. package/public/app.js +95 -0
  10. package/public/apple-touch-icon.png +0 -0
  11. package/public/assets/icon-120.png +0 -0
  12. package/public/assets/icon-16.png +0 -0
  13. package/public/assets/icon-180.png +0 -0
  14. package/public/assets/icon-192.png +0 -0
  15. package/public/assets/icon-256.png +0 -0
  16. package/public/assets/icon-32.png +0 -0
  17. package/public/assets/icon-48.png +0 -0
  18. package/public/assets/icon-512.png +0 -0
  19. package/public/assets/og-remcp.png +0 -0
  20. package/public/authorize.html +70 -0
  21. package/public/docs.html +58 -0
  22. package/public/favicon-16x16.png +0 -0
  23. package/public/favicon-32x32.png +0 -0
  24. package/public/favicon-48x48.png +0 -0
  25. package/public/favicon.ico +0 -0
  26. package/public/index.html +130 -0
  27. package/public/oauth-app.js +54 -0
  28. package/public/privacy.html +61 -0
  29. package/public/remcp-darkmode-16x16.png +0 -0
  30. package/public/remcp-darkmode-192x192.png +0 -0
  31. package/public/remcp-darkmode-32x32.png +0 -0
  32. package/public/remcp-darkmode-48x48.png +0 -0
  33. package/public/remcp-darkmode-512x512.png +0 -0
  34. package/public/security.html +59 -0
  35. package/public/site.webmanifest +23 -0
  36. package/public/style.css +231 -0
  37. package/public/support.html +57 -0
  38. package/public/terms.html +62 -0
  39. package/skills/remcp-operator/SKILL.md +36 -0
  40. package/src/agent.mjs +89 -0
  41. package/src/auth.mjs +70 -0
  42. package/src/cli.mjs +90 -0
  43. package/src/config.mjs +51 -0
  44. package/src/db.mjs +99 -0
  45. package/src/mcp.mjs +90 -0
  46. package/src/oauth.mjs +145 -0
  47. package/src/relay.mjs +97 -0
  48. package/src/review-sandbox.mjs +96 -0
  49. package/src/server.mjs +158 -0
  50. package/src/tool-catalog.json +836 -0
  51. package/src/util.mjs +24 -0
@@ -0,0 +1,231 @@
1
+ :root {
2
+ --bg: #f7f7f5;
3
+ --surface: #ffffff;
4
+ --surface-subtle: #f1f1ee;
5
+ --ink: #171719;
6
+ --muted: #68686f;
7
+ --line: #e1e1dc;
8
+ --line-strong: #cfcfc8;
9
+ --accent: #4c50d8;
10
+ --accent-hover: #3f43c3;
11
+ --accent-soft: #f0f0ff;
12
+ --danger: #b42318;
13
+ --danger-soft: #fff2f0;
14
+ --ok: #16875b;
15
+ --ok-soft: #edf8f3;
16
+ --radius: 12px;
17
+ --container: 1180px;
18
+ color: var(--ink);
19
+ background: var(--bg);
20
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
+ font-synthesis: none;
22
+ text-rendering: optimizeLegibility;
23
+ }
24
+
25
+ * { box-sizing: border-box; }
26
+ html { scroll-behavior: smooth; background: var(--bg); }
27
+ body { margin: 0; min-width: 320px; background: var(--bg); color: var(--ink); line-height: 1.55; -webkit-font-smoothing: antialiased; }
28
+ a { color: inherit; text-decoration: none; }
29
+ button, input { font: inherit; }
30
+ button, a { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
31
+ button { cursor: pointer; }
32
+ button:disabled { cursor: not-allowed; opacity: .58; }
33
+ img, svg { display: block; }
34
+ .hidden { display: none !important; }
35
+
36
+ .skip-link { position: fixed; top: 12px; left: 12px; z-index: 100; transform: translateY(-160%); padding: 10px 14px; border-radius: 8px; background: var(--ink); color: #fff; font-weight: 650; transition: transform 140ms ease; }
37
+ .skip-link:focus { transform: translateY(0); }
38
+
39
+ .site-header { min-height: 68px; display: flex; align-items: center; justify-content: space-between; gap: 28px; padding: 0 max(20px, calc((100vw - var(--container)) / 2)); border-bottom: 1px solid var(--line); background: rgba(247, 247, 245, .94); }
40
+ .wordmark { display: inline-flex; align-items: center; gap: 9px; min-width: 0; font-size: 15px; font-weight: 760; letter-spacing: -.025em; }
41
+ .wordmark span[translate="no"] { white-space: nowrap; }
42
+ .brand-mark { width: 30px; height: 30px; flex: 0 0 30px; border-radius: 8px; }
43
+ .site-header nav, .site-footer nav { display: flex; align-items: center; gap: 22px; }
44
+ .site-header nav a, .site-footer nav a { color: var(--muted); font-size: 14px; font-weight: 550; }
45
+ .site-header nav a:hover, .site-footer nav a:hover, .site-header nav a[aria-current="page"] { color: var(--ink); }
46
+ .site-header nav a[aria-current="page"] { font-weight: 680; }
47
+
48
+ main { display: block; }
49
+ .hero { max-width: var(--container); min-height: 650px; margin: 0 auto; padding: clamp(72px, 9vw, 112px) 20px; display: grid; grid-template-columns: minmax(0, 1.03fr) minmax(360px, .97fr); align-items: center; gap: clamp(48px, 7vw, 96px); }
50
+ .hero-copy { max-width: 650px; }
51
+ .eyebrow { margin: 0 0 16px; color: var(--accent); font-size: 12px; font-weight: 760; letter-spacing: .13em; text-transform: uppercase; }
52
+ .hero h1, .section-heading h2, .final-cta h2, .prose h1 { text-wrap: balance; }
53
+ .hero h1 { margin: 0; max-width: 680px; font-size: clamp(52px, 6.2vw, 82px); line-height: .98; letter-spacing: -.06em; }
54
+ .lede { max-width: 610px; margin: 25px 0 0; color: var(--muted); font-size: clamp(18px, 1.8vw, 21px); line-height: 1.58; text-wrap: pretty; }
55
+ .hero-actions { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 30px; }
56
+
57
+ .button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; border: 1px solid var(--ink); border-radius: 9px; background: var(--ink); color: #fff; font-weight: 680; line-height: 1.2; transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease; }
58
+ .button:hover { background: #29292d; border-color: #29292d; }
59
+ .button:active { transform: translateY(1px); }
60
+ .button.secondary { border-color: var(--line-strong); background: var(--surface); color: var(--ink); }
61
+ .button.secondary:hover { border-color: #b8b8b1; background: var(--surface-subtle); }
62
+ .button.ghost { border-color: transparent; background: transparent; color: var(--muted); }
63
+ .button.ghost:hover { border-color: var(--line); background: var(--surface-subtle); color: var(--ink); }
64
+ .text-link { display: inline-flex; align-items: center; gap: 6px; padding: 7px 0; color: var(--ink); font-weight: 650; border-bottom: 1px solid var(--line-strong); }
65
+ .text-link:hover { border-color: var(--ink); }
66
+
67
+ .google-button { position: relative; width: min(100%, 400px); min-height: 44px; padding: 9px 16px 9px 44px; border: 1px solid #747775; border-radius: 4px; background: #fff; color: #1f1f1f; font-family: Roboto, Arial, sans-serif; font-size: 14px; font-weight: 500; letter-spacing: .1px; box-shadow: none; }
68
+ .google-button:hover { border-color: #6f7274; background: #f8fafd; color: #1f1f1f; }
69
+ .google-button:active { background: #f1f3f4; transform: none; }
70
+ .google-icon { position: absolute; left: 12px; top: 50%; width: 20px; height: 20px; transform: translateY(-50%); }
71
+
72
+ .hero-visual { width: 100%; max-width: 520px; margin-left: auto; }
73
+ .node { min-height: 62px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 17px 18px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
74
+ .node span { font-weight: 690; }
75
+ .node b { color: var(--muted); font-size: 12px; font-weight: 600; }
76
+ .node.relay { border-color: #aeb0f4; background: #fbfbff; }
77
+ .node.device-node { position: relative; padding-right: 94px; }
78
+ .node .pulse { position: absolute; right: 16px; padding: 4px 8px; border-radius: 999px; background: var(--ok-soft); color: var(--ok); font-size: 11px; font-style: normal; font-weight: 650; }
79
+ .rail { position: relative; height: 58px; display: grid; place-items: center; }
80
+ .rail span { position: absolute; inset: 0 auto; width: 1px; background: var(--line-strong); }
81
+ .rail i { position: relative; padding: 3px 8px; background: var(--bg); color: var(--muted); font-size: 11px; font-style: normal; }
82
+
83
+ .principles { max-width: var(--container); margin: 0 auto; padding: 0 20px 72px; display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line); }
84
+ .principles > div { min-height: 210px; padding: 36px 34px 0 0; }
85
+ .principles > div + div { padding-left: 34px; border-left: 1px solid var(--line); }
86
+ .principles span { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
87
+ .principles h2 { margin: 28px 0 9px; font-size: 21px; line-height: 1.2; letter-spacing: -.025em; }
88
+ .principles p { max-width: 330px; margin: 0; color: var(--muted); font-size: 15px; }
89
+
90
+ .workspace { padding: clamp(72px, 9vw, 112px) 20px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--surface); }
91
+ .workspace > * { max-width: var(--container); margin-left: auto; margin-right: auto; }
92
+ .section-heading { max-width: 720px; margin-bottom: 45px; }
93
+ .section-heading h2, .final-cta h2 { margin: 0; font-size: clamp(38px, 5vw, 58px); line-height: 1.02; letter-spacing: -.05em; }
94
+ .section-heading > p:last-child, .final-cta > p { max-width: 620px; margin: 16px 0 0; color: var(--muted); font-size: 18px; }
95
+ .workspace-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
96
+ .auth-pane, .endpoint-pane { min-width: 0; padding: 34px 0 38px; }
97
+ .auth-pane { padding-right: 48px; border-right: 1px solid var(--line); }
98
+ .endpoint-pane { padding-left: 48px; }
99
+ .auth-pane h3, .endpoint-pane h3, .pair-panel h3, .devices-panel h3, .oauth-card h2 { margin: 0 0 9px; font-size: 20px; line-height: 1.25; letter-spacing: -.025em; }
100
+ .stack { display: flex; flex-direction: column; gap: 10px; }
101
+ .muted { color: var(--muted); }
102
+ label { display: block; margin: 10px 0 6px; color: #343438; font-size: 13px; font-weight: 650; }
103
+ .stack label { margin: 3px 0 -4px; }
104
+ input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); color: var(--ink); transition: border-color 140ms ease, box-shadow 140ms ease; }
105
+ input:hover { border-color: #b9b9b2; }
106
+ input:focus-visible { border-color: var(--accent); }
107
+ .button-row { display: flex; gap: 9px; margin-top: 5px; }
108
+ .divider { display: flex; align-items: center; gap: 11px; margin: 7px 0; color: var(--muted); font-size: 12px; }
109
+ .divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
110
+ .error { min-height: 20px; margin: 2px 0 0; color: var(--danger); font-size: 13px; }
111
+ .identity { display: flex; align-items: center; gap: 11px; }
112
+ .identity small { display: block; color: var(--muted); }
113
+ .status-dot { width: 9px; height: 9px; flex: 0 0 9px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px var(--ok-soft); }
114
+
115
+ .command-row, .pairing-output { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 9px; align-items: start; margin-top: 20px; }
116
+ .command-row code, .pairing-output code, .code-block { overflow: auto; padding: 12px 13px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-subtle); font: 12.5px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; }
117
+ .copy-button { min-height: 42px; padding: 9px 12px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); color: var(--ink); font-weight: 650; }
118
+ .copy-button:hover { background: var(--surface-subtle); }
119
+ .endpoint-facts { margin: 28px 0 0; border-top: 1px solid var(--line); }
120
+ .endpoint-facts div { display: flex; justify-content: space-between; gap: 20px; padding: 12px 0; border-bottom: 1px solid var(--line); }
121
+ .endpoint-facts dt { color: var(--muted); }
122
+ .endpoint-facts dd { margin: 0; text-align: right; font-weight: 650; }
123
+ .device-workspace { margin-top: 52px; }
124
+ .pair-panel { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 20px; align-items: center; padding-bottom: 34px; border-bottom: 1px solid var(--line); }
125
+ .pairing-output { grid-column: 1 / -1; }
126
+ .pairing-output p { grid-column: 1 / -1; margin: 0; }
127
+ .devices-panel { padding-top: 34px; }
128
+ .panel-title { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; }
129
+ .device-list { margin-top: 14px; border-top: 1px solid var(--line); }
130
+ .device { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 18px; padding: 17px 0; border-bottom: 1px solid var(--line); }
131
+ .device strong { font-size: 15px; }
132
+ .device .muted { overflow-wrap: anywhere; font-size: 13px; }
133
+ .online, .offline { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 12px; }
134
+ .online { color: var(--ok); }
135
+ .offline { color: var(--muted); }
136
+ .danger { min-height: 38px; padding: 8px 11px; border: 1px solid #e7c3be; border-radius: 8px; background: transparent; color: var(--danger); font-weight: 650; }
137
+ .danger:hover { background: var(--danger-soft); }
138
+
139
+ .final-cta { max-width: var(--container); margin: 0 auto; padding: clamp(72px, 9vw, 112px) 20px; }
140
+ .final-cta .text-link { margin-top: 24px; }
141
+ .site-footer { max-width: var(--container); min-height: 86px; margin: 0 auto; padding: 22px 20px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 26px; border-top: 1px solid var(--line); font-size: 13px; }
142
+ .site-footer p { margin: 0; color: var(--muted); text-align: right; }
143
+
144
+ .doc-shell { width: min(100%, var(--container)); margin: 0 auto; padding: 64px 20px 112px; display: grid; grid-template-columns: 190px minmax(0, 760px); gap: clamp(48px, 8vw, 96px); }
145
+ .doc-nav { position: sticky; top: 28px; align-self: start; display: flex; flex-direction: column; gap: 4px; }
146
+ .doc-nav a { padding: 8px 10px; border-radius: 7px; color: var(--muted); font-size: 14px; font-weight: 550; }
147
+ .doc-nav a:hover { background: var(--surface-subtle); color: var(--ink); }
148
+ .doc-nav a[aria-current="page"] { background: var(--surface-subtle); color: var(--ink); font-weight: 680; }
149
+ .prose { min-width: 0; }
150
+ .prose h1 { margin: 0 0 22px; max-width: 740px; font-size: clamp(42px, 5.5vw, 62px); line-height: 1.02; letter-spacing: -.05em; }
151
+ .prose h2 { scroll-margin-top: 28px; margin: 52px 0 12px; font-size: 25px; line-height: 1.2; letter-spacing: -.035em; }
152
+ .prose h3 { scroll-margin-top: 28px; margin: 31px 0 8px; font-size: 18px; }
153
+ .prose p, .prose li { color: #505057; font-size: 16px; text-wrap: pretty; }
154
+ .prose p { margin: 0 0 16px; }
155
+ .prose a { color: #3538ae; text-decoration: underline; text-decoration-color: #a5a7e9; text-underline-offset: 3px; }
156
+ .prose a:hover { text-decoration-color: #3538ae; }
157
+ .prose pre { overflow: auto; margin: 18px 0 24px; padding: 16px; border-radius: 10px; background: #1e1e21; color: #f4f4f2; }
158
+ .prose code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .9em; }
159
+ .prose :not(pre) > code { padding: 2px 5px; border-radius: 5px; background: var(--surface-subtle); color: #34343a; }
160
+ .prose hr { margin: 46px 0; border: 0; border-top: 1px solid var(--line); }
161
+ .prose .notice { margin: 28px 0; padding: 16px 18px; border: 1px solid #d8d8f7; border-radius: 9px; background: var(--accent-soft); }
162
+ .prose .notice p { margin: 5px 0 0; }
163
+
164
+ .oauth-shell { min-height: calc(100svh - 68px); display: grid; place-items: center; padding: 56px 20px 80px; }
165
+ .oauth-card { width: min(100%, 520px); padding: 30px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); }
166
+ .oauth-card .eyebrow { margin-bottom: 11px; }
167
+ .oauth-card > p:first-child { margin-top: 0; }
168
+ .oauth-card .google-button { width: 100%; }
169
+ .scope-list { margin: 24px 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }
170
+ .scope-list li { display: flex; justify-content: space-between; gap: 20px; padding: 13px 0; border-bottom: 1px solid var(--line); }
171
+ .scope-list strong { color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; font-weight: 600; }
172
+ .oauth-actions { display: flex; gap: 9px; margin-top: 18px; }
173
+ .oauth-meta { overflow-wrap: anywhere; color: var(--muted); font-size: 12px; }
174
+ .footer-mini { margin: 26px 0 0; color: var(--muted); font-size: 12px; }
175
+ .footer-mini a { text-decoration: underline; text-underline-offset: 3px; }
176
+
177
+ input:focus-visible, button:focus-visible, a:focus-visible { outline: 3px solid rgba(76, 80, 216, .28); outline-offset: 2px; }
178
+
179
+ @media (max-width: 900px) {
180
+ .hero { min-height: auto; grid-template-columns: 1fr; padding-top: 76px; padding-bottom: 76px; }
181
+ .hero-copy { max-width: 700px; }
182
+ .hero-visual { max-width: 680px; margin: 4px 0 0; }
183
+ .principles { grid-template-columns: 1fr; padding-bottom: 58px; }
184
+ .principles > div, .principles > div + div { min-height: auto; padding: 28px 0; border-left: 0; border-bottom: 1px solid var(--line); }
185
+ .principles > div:last-child { border-bottom: 0; }
186
+ .workspace-grid { grid-template-columns: 1fr; }
187
+ .auth-pane { padding-right: 0; border-right: 0; border-bottom: 1px solid var(--line); }
188
+ .endpoint-pane { padding-left: 0; }
189
+ .doc-shell { grid-template-columns: 1fr; gap: 38px; padding-top: 42px; }
190
+ .doc-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: 4px; padding-bottom: 18px; border-bottom: 1px solid var(--line); }
191
+ .site-footer { grid-template-columns: 1fr; }
192
+ .site-footer nav { flex-wrap: wrap; }
193
+ .site-footer p { text-align: left; }
194
+ }
195
+
196
+ @media (max-width: 620px) {
197
+ .site-header { min-height: 62px; padding: 0 16px; }
198
+ .site-header { gap: 12px; }
199
+ .site-header nav { gap: 10px; }
200
+ .site-header nav a { font-size: 12.5px; }
201
+ .hero { padding: 58px 18px 64px; }
202
+ .hero h1 { font-size: clamp(46px, 14vw, 60px); }
203
+ .hero-actions { align-items: stretch; flex-direction: column; gap: 10px; }
204
+ .hero-actions .button, .hero-actions .text-link { width: 100%; justify-content: center; }
205
+ .node { align-items: flex-start; flex-direction: column; gap: 3px; }
206
+ .node.device-node { padding-right: 18px; }
207
+ .node .pulse { top: 15px; }
208
+ .workspace, .final-cta { padding-left: 18px; padding-right: 18px; }
209
+ .button-row, .oauth-actions { flex-direction: column; }
210
+ .button-row .button, .oauth-actions .button { width: 100%; }
211
+ .pair-panel { grid-template-columns: 1fr; }
212
+ .pair-panel .button { justify-self: start; }
213
+ .panel-title { align-items: flex-start; flex-direction: column; }
214
+ .command-row, .pairing-output { grid-template-columns: 1fr; }
215
+ .copy-button { justify-self: start; }
216
+ .endpoint-facts div { align-items: flex-start; flex-direction: column; gap: 2px; }
217
+ .endpoint-facts dd { text-align: left; }
218
+ .site-footer { padding: 24px 18px; }
219
+ .site-footer nav { gap: 14px; }
220
+ .doc-shell { padding: 34px 18px 80px; }
221
+ .doc-nav { overflow-x: auto; flex-wrap: nowrap; margin: 0 -18px; padding: 0 18px 16px; }
222
+ .doc-nav a { white-space: nowrap; }
223
+ .oauth-shell { min-height: calc(100svh - 62px); padding: 28px 14px 60px; place-items: start center; }
224
+ .oauth-card { padding: 22px 18px; }
225
+ .scope-list li { align-items: flex-start; flex-direction: column; gap: 4px; }
226
+ }
227
+
228
+ @media (prefers-reduced-motion: reduce) {
229
+ html { scroll-behavior: auto; }
230
+ *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
231
+ }
@@ -0,0 +1,57 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="color-scheme" content="light">
7
+ <meta name="theme-color" content="#f7f7f5">
8
+ <title>Support · ReMCP</title>
9
+ <meta name="description" content="Troubleshoot ReMCP sign-in, device pairing, agent connectivity and MCP authorization.">
10
+ <link rel="preconnect" href="https://www.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="/style.css?v=b92ac4c9219a">
12
+ <meta property="og:site_name" content="ReMCP">
13
+ <meta property="og:image" content="https://remcp.delio24.com/assets/og-remcp.png">
14
+ <link rel="icon" href="/favicon.ico" sizes="any">
15
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" media="(prefers-color-scheme: light)">
16
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" media="(prefers-color-scheme: light)">
17
+ <link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" media="(prefers-color-scheme: light)">
18
+ <link rel="icon" type="image/png" sizes="16x16" href="/remcp-darkmode-16x16.png" media="(prefers-color-scheme: dark)">
19
+ <link rel="icon" type="image/png" sizes="32x32" href="/remcp-darkmode-32x32.png" media="(prefers-color-scheme: dark)">
20
+ <link rel="icon" type="image/png" sizes="48x48" href="/remcp-darkmode-48x48.png" media="(prefers-color-scheme: dark)">
21
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
22
+ <link rel="manifest" href="/site.webmanifest">
23
+ </head>
24
+ <body>
25
+ <a class="skip-link" href="#main">Skip to content</a>
26
+ <header class="site-header">
27
+ <a class="wordmark" href="/" aria-label="ReMCP home">
28
+ <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
29
+ <span translate="no">ReMCP</span>
30
+ </a>
31
+ <nav aria-label="Primary"><a href="/docs">Docs</a><a href="/security">Security</a><a href="/support" aria-current="page">Support</a></nav>
32
+ </header>
33
+ <main id="main" class="doc-shell">
34
+ <aside class="doc-nav" aria-label="Documentation"><a href="/docs">Setup</a><a href="/security">Security</a><a href="/support" aria-current="page">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a></aside>
35
+ <article class="prose">
36
+ <p class="eyebrow">Support</p>
37
+ <h1>Troubleshoot ReMCP.</h1>
38
+ <p>Most connection failures fall into one of three layers: account authentication, device pairing, or the local device agent.</p>
39
+ <h2>Device shows offline</h2><p>Verify the ReMCP agent service is running and outbound HTTPS/WSS access to <code>remcp.delio24.com</code> is allowed. Re-pair the device if its credential was revoked.</p>
40
+ <h2>OAuth does not complete</h2><p>Return to the authorization screen, sign in to the intended ReMCP account, review the requested access, and choose <strong>Authorize ReMCP Access</strong>.</p>
41
+ <h2>MCP tool cannot run</h2><p>Confirm the target computer is online and use <code>list_devices</code> to obtain its current device id. Device tools require that id.</p>
42
+ <h2>Pairing code expired</h2><p>Generate a new pairing command from the workspace. Pairing codes are intentionally short-lived and single-use.</p>
43
+ <h2>Need deployment help?</h2><p>For a self-hosted deployment, contact the operator who provisioned your ReMCP instance and include the affected layer: sign-in, pairing, agent connectivity, or MCP authorization.</p>
44
+ </article>
45
+ </main>
46
+ <footer class="site-footer">
47
+ <div class="wordmark">
48
+ <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
49
+ <span translate="no">ReMCP</span>
50
+ </div>
51
+ <nav aria-label="Footer">
52
+ <a href="/docs">Docs</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a>
53
+ </nav>
54
+ <p>Open-source remote MCP infrastructure.</p>
55
+ </footer>
56
+ </body>
57
+ </html>
@@ -0,0 +1,62 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="color-scheme" content="light">
7
+ <meta name="theme-color" content="#f7f7f5">
8
+ <title>Terms of Service · ReMCP</title>
9
+ <meta name="description" content="Terms for using the hosted ReMCP service and its remote-computer capabilities.">
10
+ <link rel="preconnect" href="https://www.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="/style.css?v=b92ac4c9219a">
12
+ <meta property="og:site_name" content="ReMCP">
13
+ <meta property="og:image" content="https://remcp.delio24.com/assets/og-remcp.png">
14
+ <link rel="icon" href="/favicon.ico" sizes="any">
15
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" media="(prefers-color-scheme: light)">
16
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" media="(prefers-color-scheme: light)">
17
+ <link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" media="(prefers-color-scheme: light)">
18
+ <link rel="icon" type="image/png" sizes="16x16" href="/remcp-darkmode-16x16.png" media="(prefers-color-scheme: dark)">
19
+ <link rel="icon" type="image/png" sizes="32x32" href="/remcp-darkmode-32x32.png" media="(prefers-color-scheme: dark)">
20
+ <link rel="icon" type="image/png" sizes="48x48" href="/remcp-darkmode-48x48.png" media="(prefers-color-scheme: dark)">
21
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
22
+ <link rel="manifest" href="/site.webmanifest">
23
+ </head>
24
+ <body>
25
+ <a class="skip-link" href="#main">Skip to content</a>
26
+ <header class="site-header">
27
+ <a class="wordmark" href="/" aria-label="ReMCP home">
28
+ <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
29
+ <span translate="no">ReMCP</span>
30
+ </a>
31
+ <nav aria-label="Primary"><a href="/docs">Docs</a><a href="/security">Security</a><a href="/support">Support</a></nav>
32
+ </header>
33
+ <main id="main" class="doc-shell">
34
+ <aside class="doc-nav" aria-label="Legal and policy navigation"><a href="/docs">Setup</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a></aside>
35
+ <article class="prose">
36
+ <p class="eyebrow">Legal</p>
37
+ <h1>Terms of Service</h1>
38
+ <p><strong>Effective September 16, 2026.</strong> These Terms apply to the hosted ReMCP service at remcp.delio24.com. The open-source software may also be self-hosted under its applicable software license.</p>
39
+ <h2>Your account and devices</h2><p>You may connect only computers and accounts that you own or are authorized to administer. You are responsible for protecting your sign-in account, reviewing paired devices, and revoking devices or OAuth grants you no longer trust.</p>
40
+ <h2>Remote actions</h2><p>ReMCP can expose powerful local operations, including reading and modifying files and starting or terminating processes. Some actions can be destructive or can affect external systems when a command run on your computer reaches them. You are responsible for reviewing and authorizing requested actions and for maintaining appropriate backups and operating-system permissions.</p>
41
+ <h2>Acceptable use</h2><p>You may not use ReMCP to access systems without authorization, distribute malware, evade security controls, interfere with other users, conduct credential theft, or violate applicable law or third-party rights. You may not attempt to access devices belonging to another ReMCP account.</p>
42
+ <h2>Service operation</h2><p>The hosted service may be updated, rate-limited, suspended or discontinued to protect users, respond to abuse, maintain security or operate the service. ReMCP does not guarantee uninterrupted availability. Self-hosted deployments are operated and secured by their administrators.</p>
43
+ <h2>Third-party services and software</h2><p>Authentication may rely on Firebase/Google, and device functionality may rely on open-source dependencies. Those services and packages are subject to their own terms and licenses. ReMCP does not grant rights to third-party systems that you access from a paired device.</p>
44
+ <h2>Open-source software</h2><p>The ReMCP source distribution includes software under open-source licenses. License notices in the source distribution govern those components independently of these hosted-service Terms.</p>
45
+ <h2>No warranty</h2><p>To the maximum extent permitted by applicable law, the service and software are provided on an “as is” and “as available” basis without warranties of uninterrupted operation, fitness for a particular purpose, or freedom from all defects. Nothing in these Terms limits rights that cannot legally be excluded.</p>
46
+ <h2>Responsibility for losses</h2><p>You are responsible for commands and changes made through devices you authorize. To the maximum extent permitted by applicable law, ReMCP is not responsible for indirect, incidental or consequential losses arising from device changes, lost data, third-party systems or service interruptions. Mandatory consumer or statutory rights remain unaffected.</p>
47
+ <h2>Termination</h2><p>You may stop using the service and revoke your devices at any time. Access may be suspended for security, abuse or legal reasons. Provisions that by their nature should survive termination, including responsibility for prior actions and license obligations, continue to apply.</p>
48
+ <h2>Changes</h2><p>Material changes to these Terms will be published at this URL with a new effective date. Continued use after changes become effective constitutes acceptance where permitted by law.</p>
49
+ </article>
50
+ </main>
51
+ <footer class="site-footer">
52
+ <div class="wordmark">
53
+ <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
54
+ <span translate="no">ReMCP</span>
55
+ </div>
56
+ <nav aria-label="Footer">
57
+ <a href="/docs">Docs</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a>
58
+ </nav>
59
+ <p>Open-source remote MCP infrastructure.</p>
60
+ </footer>
61
+ </body>
62
+ </html>
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: remcp-operator
3
+ description: Safely operate computers paired through ReMCP. Use when the user asks to inspect or change files, directories, local processes, terminal sessions, or ReMCP device state on one of their paired computers.
4
+ ---
5
+
6
+ # ReMCP Operator
7
+
8
+ Use ReMCP only when the request actually needs a paired computer. Do not invoke ReMCP for general knowledge, writing, weather, web research, or conceptual questions that can be answered without the user's device.
9
+
10
+ ## Device selection
11
+
12
+ 1. Call `list_devices` before the first device operation unless a current ReMCP device id is already unambiguous in the conversation.
13
+ 2. Prefer an online device whose user-facing name or hostname matches the request.
14
+ 3. If more than one online device plausibly matches and choosing the wrong machine could change state, ask the user which device to use.
15
+
16
+ ## Read before write
17
+
18
+ Inspect the smallest amount of device state needed to understand the request before changing it. Prefer `list_directory`, `get_file_info`, `read_file`, `list_processes`, or existing process output before a write or terminal mutation.
19
+ ## Changes
20
+
21
+ - Use the narrowest tool that performs the requested change. Prefer `edit_block`, `write_file`, or `move_file` over a shell command when the dedicated tool expresses the action clearly.
22
+ - Do not broaden a requested path, command, or target without a reason tied to the user's task.
23
+ - Treat `set_config_value`, file replacement, process termination, and generic terminal commands as state-changing actions.
24
+ - Generic terminal commands may also affect external services. Keep commands scoped to the user's stated goal and surface consequential effects before execution when they are not already clear from the request.
25
+
26
+ ## Verification
27
+
28
+ After a change, use the cheapest relevant read to verify the result. Examples: re-read the edited range, list the destination directory after a move, or read process output after starting a command.
29
+
30
+ ## Long-running processes
31
+
32
+ Use `start_process` once and then `read_process_output` or `interact_with_process` for the existing session. Avoid starting duplicate long-running processes just to obtain new output.
33
+
34
+ ## Out-of-scope requests
35
+
36
+ Do not use ReMCP to access a computer the user has not paired or is not authorized to control. Do not ask for passwords, MFA codes, private keys, or other credentials through ReMCP tool inputs.
package/src/agent.mjs ADDED
@@ -0,0 +1,89 @@
1
+ import os from 'node:os';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import WebSocket from 'ws';
7
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
9
+
10
+ function desktopCommanderEntry() {
11
+ try { return fileURLToPath(import.meta.resolve('@wonderwhy-er/desktop-commander')); } catch {}
12
+ const configured = String(process.env.REMCP_DESKTOP_COMMANDER_ENTRY || '').trim();
13
+ const candidates = [
14
+ configured,
15
+ '/usr/lib/node_modules/@wonderwhy-er/desktop-commander/dist/index.js',
16
+ '/usr/local/lib/node_modules/@wonderwhy-er/desktop-commander/dist/index.js',
17
+ path.join(os.homedir(), '.hermes/node/lib/node_modules/@wonderwhy-er/desktop-commander/dist/index.js'),
18
+ ].filter(Boolean);
19
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
20
+ throw new Error('@wonderwhy-er/desktop-commander is not installed');
21
+ }
22
+
23
+ export async function runAgent(options) {
24
+ const serverUrl = String(options.serverUrl || '').replace(/\/$/, '');
25
+ const deviceToken = String(options.deviceToken || '');
26
+ const deviceId = String(options.deviceId || '');
27
+ const deviceName = String(options.deviceName || os.hostname());
28
+ if (!serverUrl || !deviceToken || !deviceId) throw new Error('serverUrl, deviceToken and deviceId are required');
29
+ const agentUrl = serverUrl.replace(/^http/, 'ws') + '/agent';
30
+ const reconnectMs = Number(options.reconnectMs || 3000);
31
+ let stopping = false;
32
+ let activeSocket;
33
+
34
+ const mcp = new Client({ name: 'remcp-agent', version: '0.1.0' });
35
+ const transport = new StdioClientTransport({ command: process.execPath, args: [desktopCommanderEntry()] });
36
+ await mcp.connect(transport);
37
+ console.log('Local Desktop Commander MCP ready');
38
+
39
+ async function respond(ws, message) {
40
+ try {
41
+ let result;
42
+ if (message.method === 'tools/list') result = await mcp.listTools();
43
+ else if (message.method === 'tools/call') result = await mcp.callTool(message.params);
44
+ else if (message.method === 'ping') result = { ok: true, hostname: os.hostname(), platform: process.platform, arch: process.arch, uptimeSeconds: Math.floor(os.uptime()) };
45
+ else throw new Error(`Unsupported relay method: ${message.method}`);
46
+ ws.send(JSON.stringify({ type: 'response', id: message.id, result }));
47
+ } catch (error) {
48
+ ws.send(JSON.stringify({ type: 'response', id: message.id, error: { message: error instanceof Error ? error.message : String(error) } }));
49
+ }
50
+ }
51
+
52
+ function connect() {
53
+ if (stopping) return;
54
+ const ws = new WebSocket(agentUrl, { headers: { Authorization: `Bearer ${deviceToken}` } });
55
+ activeSocket = ws;
56
+ ws.on('open', () => {
57
+ ws.send(JSON.stringify({ type: 'hello', deviceId, deviceName, hostname: os.hostname(), platform: process.platform, arch: process.arch, agentVersion: '0.1.0' }));
58
+ console.log(`Connected to ${agentUrl} as ${deviceName} (${deviceId})`);
59
+ });
60
+ ws.on('message', raw => {
61
+ let message;
62
+ try { message = JSON.parse(raw.toString()); } catch { return; }
63
+ if (message?.type === 'request') void respond(ws, message);
64
+ });
65
+ ws.on('close', () => { if (!stopping) setTimeout(connect, reconnectMs); });
66
+ ws.on('error', error => console.error(`ReMCP relay: ${error.message}`));
67
+ }
68
+
69
+ async function stop() {
70
+ if (stopping) return;
71
+ stopping = true;
72
+ try { activeSocket?.close(); } catch {}
73
+ try { await mcp.close(); } catch {}
74
+ try { await transport.close(); } catch {}
75
+ }
76
+ process.once('SIGINT', () => void stop().finally(() => process.exit(0)));
77
+ process.once('SIGTERM', () => void stop().finally(() => process.exit(0)));
78
+ connect();
79
+ return { stop };
80
+ }
81
+
82
+ if (import.meta.url === `file://${process.argv[1]}` && process.env.REMCP_SERVER_URL) {
83
+ await runAgent({
84
+ serverUrl: process.env.REMCP_SERVER_URL,
85
+ deviceToken: process.env.REMCP_DEVICE_TOKEN,
86
+ deviceId: process.env.REMCP_DEVICE_ID,
87
+ deviceName: process.env.REMCP_DEVICE_NAME,
88
+ });
89
+ }
package/src/auth.mjs ADDED
@@ -0,0 +1,70 @@
1
+ import { createRemoteJWKSet, jwtVerify, SignJWT } from 'jose';
2
+ import { config } from './config.mjs';
3
+ import { bearer } from './util.mjs';
4
+ import { upsertUser } from './db.mjs';
5
+
6
+ const firebaseJwks = createRemoteJWKSet(new URL('https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com'));
7
+ const signingKey = new TextEncoder().encode(config.tokenSecret);
8
+
9
+ export async function verifyFirebaseToken(token) {
10
+ const { payload } = await jwtVerify(token, firebaseJwks, {
11
+ issuer: `https://securetoken.google.com/${config.firebaseProjectId}`,
12
+ audience: config.firebaseProjectId,
13
+ });
14
+ if (!payload.sub) throw new Error('Firebase token has no subject');
15
+ const email = typeof payload.email === 'string' ? payload.email.toLowerCase() : '';
16
+ const uid = config.ownerEmail && email === config.ownerEmail ? `owner:${config.ownerEmail}` : payload.sub;
17
+ return {
18
+ uid,
19
+ firebaseUid: payload.sub,
20
+ email,
21
+ name: typeof payload.name === 'string' ? payload.name : '',
22
+ };
23
+ }
24
+
25
+ export async function requireFirebase(req, res, next) {
26
+ try {
27
+ const token = bearer(req);
28
+ if (!token) return res.status(401).json({ error: 'firebase_auth_required' });
29
+ const user = await verifyFirebaseToken(token);
30
+ upsertUser(user);
31
+ req.user = user;
32
+ next();
33
+ } catch (error) {
34
+ res.status(401).json({ error: 'invalid_firebase_token', message: error.message });
35
+ }
36
+ }
37
+
38
+ export async function signMcpAccessToken({ uid, clientId, scope }) {
39
+ return new SignJWT({ scope, client_id: clientId, typ: 'remcp_access' })
40
+ .setProtectedHeader({ alg: 'HS256' })
41
+ .setIssuer(config.publicUrl)
42
+ .setAudience(`${config.publicUrl}/mcp`)
43
+ .setSubject(uid)
44
+ .setIssuedAt()
45
+ .setExpirationTime(`${config.accessTtlSeconds}s`)
46
+ .sign(signingKey);
47
+ }
48
+
49
+ export async function verifyMcpAccessToken(token) {
50
+ const { payload } = await jwtVerify(token, signingKey, {
51
+ issuer: config.publicUrl,
52
+ audience: `${config.publicUrl}/mcp`,
53
+ });
54
+ if (payload.typ !== 'remcp_access' || !payload.sub) throw new Error('Invalid ReMCP access token');
55
+ return { uid: payload.sub, scope: String(payload.scope || ''), clientId: String(payload.client_id || '') };
56
+ }
57
+
58
+ export async function requireMcpAuth(req, res, next) {
59
+ try {
60
+ const token = bearer(req);
61
+ if (!token) throw new Error('missing_token');
62
+ const auth = await verifyMcpAccessToken(token);
63
+ if (!auth.scope.split(/\s+/).includes('remcp:control')) throw new Error('missing_scope');
64
+ req.mcpAuth = auth;
65
+ next();
66
+ } catch {
67
+ res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${config.publicUrl}/.well-known/oauth-protected-resource/mcp"`);
68
+ res.status(401).json({ error: 'unauthorized' });
69
+ }
70
+ }