@remcp/remcp 0.1.1 → 0.1.2

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 CHANGED
@@ -1,90 +1,118 @@
1
1
  # ReMCP
2
2
 
3
- ReMCP is an open-source remote MCP bridge for computers you control. A local agent connects outbound to a ReMCP server and exposes filesystem, search, terminal and process tools through one authenticated remote MCP endpoint.
3
+ ReMCP is an open-source remote MCP bridge for computers you control. A lightweight device agent connects **outbound** to a ReMCP relay, so ChatGPT, Codex, or another MCP client can work with local files, directories, searches, terminal sessions, and processes without exposing an inbound port on the computer.
4
4
 
5
- **Hosted service:** https://remcp.delio24.com
6
- **MCP endpoint:** `https://remcp.delio24.com/mcp`
5
+ - Hosted service: <https://remcp.delio24.com>
6
+ - MCP endpoint: `https://remcp.delio24.com/mcp`
7
+ - npm: `@remcp/remcp`
8
+ - License: MIT
7
9
 
8
- ## How it works
10
+ ## Quick start
9
11
 
10
- ```text
11
- ChatGPT / MCP client ── OAuth + HTTPS ──> ReMCP server
12
-
13
- ├── WSS ──> laptop agent ──> local MCP
14
- └── WSS ──> server agent ──> local MCP
15
- ```
16
-
17
- The central service stores account/device metadata, hashed reusable credentials and OAuth state. Actual filesystem and process access stays on the paired computer.
12
+ Requires Node.js 22.5 or newer.
18
13
 
19
- ## Install and pair
20
-
21
- Requires Node.js 22.5 or newer. Generate a one-time pairing code after signing in at https://remcp.delio24.com, then run:
14
+ 1. Sign in at <https://remcp.delio24.com> and generate a one-time pairing command.
15
+ 2. Run it on the computer you want to connect:
22
16
 
23
17
  ```bash
24
- npx --yes @remcp/remcp@latest connect --server https://remcp.delio24.com --code YOUR_PAIRING_CODE --install
18
+ npx --yes @remcp/remcp@latest connect \
19
+ --server https://remcp.delio24.com \
20
+ --code YOUR_PAIRING_CODE \
21
+ --install
25
22
  ```
26
23
 
27
- `--install` creates a persistent global `remcp` CLI and Linux user service. The initial `npx` execution is only the bootstrap step.
28
-
29
- Verify:
24
+ 3. Verify the persistent agent:
30
25
 
31
26
  ```bash
32
27
  remcp --version
33
28
  remcp status
34
29
  ```
35
30
 
31
+ 4. Add the MCP endpoint to your client:
32
+
33
+ ```text
34
+ https://remcp.delio24.com/mcp
35
+ ```
36
+
37
+ Authentication uses OAuth authorization code + PKCE with rotating refresh credentials.
38
+
36
39
  ## Update
37
40
 
38
41
  ```bash
39
42
  remcp update
40
43
  ```
41
44
 
42
- This updates the global CLI to the latest published release, refreshes the compatible local runtime, and restarts the installed user service.
45
+ `remcp update` refreshes the global CLI and compatible local runtime, then restarts the installed user service.
43
46
 
44
47
  ## CLI
45
48
 
46
49
  ```text
47
50
  remcp start Run the device agent in the foreground
48
- remcp status Check pairing, version and server health
51
+ remcp status Show version, pairing and server health
49
52
  remcp doctor Alias for status
50
- remcp update Update ReMCP and restart the installed service
53
+ remcp update Update ReMCP and restart the user service
51
54
  remcp install Install or repair the user service
52
55
  remcp uninstall Remove the user service
53
56
  remcp uninstall --purge Remove the service and global ReMCP packages
54
57
  remcp --version Print the installed CLI version
55
58
  ```
56
59
 
57
- ## Self-host the server
60
+ ## Architecture
58
61
 
59
- ```bash
60
- git clone https://github.com/antonbaider/remcp.git
61
- cd remcp
62
- cp .env.example .env
63
- # Fill in your public URL and Firebase Web App configuration.
64
- docker compose up -d --build
62
+ ```text
63
+ ChatGPT / Codex / MCP client
64
+
65
+ OAuth 2.1 + HTTPS
66
+
67
+ ReMCP relay /mcp
68
+
69
+ │ authenticated WSS (outbound from device)
70
+
71
+ paired computer
72
+
73
+ └── local file / search / terminal / process runtime
65
74
  ```
66
75
 
67
- Put a TLS reverse proxy in front of ReMCP and proxy WebSocket upgrades for `/agent`. The example Compose file binds the application to localhost by default. Do not publish the internal application port directly unless you have deliberately secured it.
76
+ The hosted relay stores account/device metadata, one-way hashes of reusable credentials, OAuth client metadata, and refresh-token hashes. Device credentials are independently revocable. MCP sessions are scoped to the authenticated ReMCP account.
77
+
78
+ ## OpenAI Plugin package
68
79
 
69
- ReMCP does **not** require a Firebase Admin service-account private key. Firebase ID tokens are verified using Google's public signing keys.
80
+ The repository is prepared for the current OpenAI Plugins format:
70
81
 
71
- ## ChatGPT / remote MCP
82
+ - `plugin.json` portable Agent Plugins manifest;
83
+ - `mcp.json` — production Streamable HTTP MCP configuration;
84
+ - `skills/remcp-operator/SKILL.md` — bundled operator skill;
85
+ - `assets/remcp-icon.png` — plugin icon;
86
+ - `docs/OPENAI_PLUGIN.md` — submission and reviewer checklist;
87
+ - `submission/openai-plugin-review.json` — generated tool annotations plus five positive and three negative review cases.
72
88
 
73
- ReMCP exposes Streamable HTTP MCP at `/mcp`. Authentication uses authorization code + PKCE, Dynamic Client Registration, short-lived access tokens and rotating refresh tokens.
89
+ ReMCP has no plugin UI, so the public submission should not include UI screenshots. The remote MCP server uses a universal production URL and OAuth. See [`docs/OPENAI_PLUGIN.md`](docs/OPENAI_PLUGIN.md) before submitting.
74
90
 
75
- The repository includes `chatgpt-app-submission.json` plus a reusable ReMCP operator skill for submission/testing workflows.
76
91
  ## Security model
77
92
 
78
- - one independent credential per paired device;
79
- - device and refresh credentials are stored as one-way hashes server-side;
93
+ - no inbound port is required on paired computers;
94
+ - one revocable credential is issued per paired device;
95
+ - reusable credentials are hashed server-side;
80
96
  - pairing codes are short-lived and single-use;
81
- - MCP sessions are bound to the authenticated account;
82
- - the device agent connects outbound over WSS;
83
- - write/process tools expose explicit MCP risk annotations;
84
- - the public review sandbox is isolated per authenticated user and intentionally limits terminal commands;
85
- - central Docker deployment runs read-only with dropped Linux capabilities in the supplied Compose configuration.
97
+ - OAuth uses PKCE and rotating refresh credentials;
98
+ - OAuth metadata advertises `openid` and `email`, with a UserInfo endpoint for verified-email workspace restrictions;
99
+ - MCP tools carry explicit `readOnlyHint`, `openWorldHint`, and `destructiveHint` annotations;
100
+ - the public review fixture is isolated per authenticated account and intentionally restricts terminal commands;
101
+ - the supplied server container runs read-only with dropped Linux capabilities.
86
102
 
87
- See https://remcp.delio24.com/security for the hosted-service security overview.
103
+ ReMCP tools should not be used to request or process passwords, MFA codes, API/private keys, payment-card data, protected health information, or government identifiers. See [`SECURITY.md`](SECURITY.md) and the hosted <https://remcp.delio24.com/security> page.
104
+
105
+ ## Self-hosting
106
+
107
+ ```bash
108
+ git clone https://github.com/antonbaider/remcp.git
109
+ cd remcp
110
+ cp .env.example .env
111
+ # Fill in your public URL and Firebase Web App configuration.
112
+ docker compose up -d --build
113
+ ```
114
+
115
+ Put a TLS reverse proxy in front of ReMCP and proxy WebSocket upgrades for `/agent`. The supplied Compose configuration binds the application to localhost by default. ReMCP verifies Firebase ID tokens using Google's public signing keys and does not require a Firebase Admin private key.
88
116
 
89
117
  ## Development
90
118
 
@@ -93,8 +121,25 @@ npm ci
93
121
  npm run check
94
122
  npm test
95
123
  npm audit --omit=dev
124
+ npm pack --dry-run
125
+ ```
126
+
127
+ For server changes:
128
+
129
+ ```bash
130
+ docker build -t remcp:test .
96
131
  ```
97
132
 
133
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for contribution and security rules.
134
+
135
+ ## Project docs
136
+
137
+ - [`docs/OPENAI_PLUGIN.md`](docs/OPENAI_PLUGIN.md) — OpenAI Plugins submission
138
+ - [`SECURITY.md`](SECURITY.md) — vulnerability reporting and operational security
139
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md) — development checks
140
+ - [`CHANGELOG.md`](CHANGELOG.md) — release notes
141
+ - [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) — third-party license notices
142
+
98
143
  ## License
99
144
 
100
- ReMCP is MIT licensed. Third-party notices for the local device runtime are preserved in `THIRD_PARTY_NOTICES.md`. ReMCP is self-hostable and does not depend on a proprietary hosted relay.
145
+ ReMCP is MIT licensed. Third-party notices for the local device runtime are preserved in `THIRD_PARTY_NOTICES.md`.
Binary file
package/mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "remcp": {
5
+ "type": "streamable-http",
6
+ "url": "https://remcp.delio24.com/mcp"
7
+ }
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Self-hosted remote MCP bridge for securely controlling your own computers from MCP clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,8 +15,10 @@
15
15
  "bin",
16
16
  "src",
17
17
  "public",
18
+ "plugin.json",
19
+ "mcp.json",
20
+ "assets",
18
21
  "skills/remcp-operator",
19
- "chatgpt-app-submission.json",
20
22
  "LICENSE",
21
23
  "THIRD_PARTY_NOTICES.md",
22
24
  "README.md"
package/plugin.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "remcp",
4
+ "version": "0.1.2",
5
+ "description": "Securely use files, directories, searches, terminal sessions, and processes on computers you have paired with ReMCP.",
6
+ "homepage": "https://remcp.delio24.com",
7
+ "repository": "https://github.com/antonbaider/remcp",
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "mcp",
11
+ "remote-computer",
12
+ "developer-tools",
13
+ "files",
14
+ "terminal"
15
+ ],
16
+ "extensions": {
17
+ "com.openai": {
18
+ "interface": {
19
+ "displayName": "ReMCP",
20
+ "shortDescription": "Use your paired computers through MCP.",
21
+ "longDescription": "ReMCP securely connects ChatGPT and Codex to computers you have explicitly paired. Inspect files and directories, search local content, and manage terminal sessions and processes through an authenticated remote MCP endpoint.",
22
+ "category": "Developer Tools",
23
+ "websiteURL": "https://remcp.delio24.com",
24
+ "privacyPolicyURL": "https://remcp.delio24.com/privacy",
25
+ "termsOfServiceURL": "https://remcp.delio24.com/terms",
26
+ "defaultPrompt": [
27
+ "Show my paired ReMCP computers and tell me which ones are online.",
28
+ "On my laptop, list the files in my project folder and summarize what is there without changing anything.",
29
+ "On my server, list the running processes and help me identify the one using the most memory."
30
+ ],
31
+ "brandColor": "#4C50D8",
32
+ "composerIcon": "./assets/remcp-icon.png",
33
+ "logo": "./assets/remcp-icon.png"
34
+ }
35
+ }
36
+ }
37
+ }
@@ -56,7 +56,7 @@
56
56
  <p class="eyebrow">Access request</p>
57
57
  <h2>Authorize ReMCP Access</h2>
58
58
  <p class="muted">This MCP client is requesting access to operate computers paired to this ReMCP account.</p>
59
- <ul class="scope-list"><li><span>Control paired devices</span><strong>remcp:control</strong></li><li><span>Maintain the authorized connection</span><strong>offline_access</strong></li></ul>
59
+ <ul class="scope-list"><li><span>Identify your ReMCP account</span><strong>openid</strong></li><li><span>Share your verified email for workspace access rules</span><strong>email</strong></li><li><span>Control paired devices</span><strong>remcp:control</strong></li><li><span>Maintain the authorized connection</span><strong>offline_access</strong></li></ul>
60
60
  <p class="oauth-meta">Client: <span id="client-id"></span></p>
61
61
  <div class="oauth-actions"><button id="authorize" class="button" type="button">Authorize ReMCP Access</button><a class="button ghost" href="/">Not Now</a></div>
62
62
  </div>
@@ -41,7 +41,8 @@
41
41
  <h2>Device and security metadata</h2><p>ReMCP stores device identifiers, device names and basic platform metadata, one-way hashes of device credentials, pairing records, OAuth client metadata, and one-way hashes of refresh credentials. Pairing codes are short-lived and single-use. Raw device credentials are returned to the paired device and are not stored in plaintext by the service.</p>
42
42
  <h2>Files, commands and tool results</h2><p>When you use an MCP tool, its request and response may pass through the ReMCP relay. This can include filenames, file contents, command output and process information that you explicitly ask the paired computer to process. ReMCP does not intentionally retain those tool payloads as product data. Operational security logs may retain limited metadata such as timestamps, status codes and errors.</p>
43
43
  <h2>How data is used and shared</h2><p>Data is used to provide authentication, device pairing, OAuth authorization, request routing, abuse prevention, security and reliability. ReMCP does not sell personal information and does not serve advertising. Data may be processed by infrastructure and authentication providers only as needed to operate the service, including Firebase/Google for authentication.</p>
44
- <h2>Retention and control</h2><p>Device records remain until revoked or removed. OAuth grants remain until revoked, expired or rotated. Security and operational metadata may be retained for a limited period appropriate to troubleshooting and abuse prevention. You can revoke a paired device from the ReMCP workspace. Requests concerning account data or deletion can be made through the support channel identified on the Support page or in the app listing.</p>
44
+ <h2>Retention and control</h2><p>Pairing codes expire after 10 minutes by default. MCP access tokens expire after 1 hour by default. Refresh credentials expire after 30 days by default and are rotated on use. Device records remain until you revoke or remove the device. ReMCP does not intentionally retain MCP tool request or response payloads as product data. Hosted Nginx access and error logs are rotated daily with 14 rotated copies. Application container logs are size-bounded to three files of at most 50 MB each rather than retained for a fixed number of days. These operational logs are used only for security, reliability, and troubleshooting and are not intended to contain authentication secrets or MCP tool payloads. You can revoke a paired device from the ReMCP workspace, and account-data or deletion requests can be made through the Support page.</p>
45
+ <h2>Restricted data</h2><p>ReMCP is not intended to collect or process passwords, MFA or OTP codes, API or private keys, payment-card data, protected health information, or government identifiers through plugin tool calls. Users should keep those data out of ReMCP requests and use a safer local workflow when a requested file or command would expose them.</p>
45
46
  <h2>Security and international processing</h2><p>ReMCP uses TLS for network transport, hashes reusable credentials before storage, and scopes device access to the authenticated account. Internet services may process data in more than one country depending on infrastructure and authentication providers. See the <a href="/security">Security page</a> for the current security model.</p>
46
47
  <h2>Children</h2><p>ReMCP is a developer and systems-administration tool and is not directed to children under 13.</p>
47
48
  <h2>Changes</h2><p>Material changes to this policy will be published at this URL with an updated effective date.</p>
@@ -33,4 +33,4 @@ Use `start_process` once and then `read_process_output` or `interact_with_proces
33
33
 
34
34
  ## Out-of-scope requests
35
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.
36
+ Do not use ReMCP to access a computer the user has not paired or is not authorized to control. Do not ask for or process passwords, MFA codes, private keys, API keys, payment-card data, protected health information, government identifiers, or other restricted credentials/data through ReMCP tools. If a requested file or command would expose those categories, ask the user to use a safer local workflow instead.
package/src/auth.mjs CHANGED
@@ -18,6 +18,7 @@ export async function verifyFirebaseToken(token) {
18
18
  uid,
19
19
  firebaseUid: payload.sub,
20
20
  email,
21
+ emailVerified: payload.email_verified === true,
21
22
  name: typeof payload.name === 'string' ? payload.name : '',
22
23
  };
23
24
  }
package/src/config.mjs CHANGED
@@ -38,7 +38,7 @@ export const config = Object.freeze({
38
38
  refreshTtlSeconds: Number(process.env.REFRESH_TTL_SECONDS || 2592000),
39
39
  rpcTimeoutMs: Number(process.env.RPC_TIMEOUT_MS || 120000),
40
40
  domainVerificationToken: (process.env.OPENAI_DOMAIN_VERIFICATION_TOKEN || '').trim(),
41
- domainVerificationPath: (process.env.OPENAI_DOMAIN_VERIFICATION_PATH || '/.well-known/openai-domain-verification').trim(),
41
+ domainVerificationPath: (process.env.OPENAI_DOMAIN_VERIFICATION_PATH || '/.well-known/openai-apps-challenge').trim(),
42
42
  firebase: {
43
43
  apiKey: required('FIREBASE_API_KEY'),
44
44
  authDomain: required('FIREBASE_AUTH_DOMAIN'),
package/src/db.mjs CHANGED
@@ -9,7 +9,7 @@ export const db = new DatabaseSync(join(config.dataDir, 'remcp.sqlite'));
9
9
  db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;`);
10
10
  db.exec(`
11
11
  CREATE TABLE IF NOT EXISTS users (
12
- uid TEXT PRIMARY KEY, email TEXT, name TEXT, created_at INTEGER NOT NULL, last_login_at INTEGER NOT NULL
12
+ uid TEXT PRIMARY KEY, email TEXT, name TEXT, email_verified INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, last_login_at INTEGER NOT NULL
13
13
  );
14
14
  CREATE TABLE IF NOT EXISTS pair_codes (
15
15
  code_hash TEXT PRIMARY KEY, uid TEXT NOT NULL REFERENCES users(uid) ON DELETE CASCADE,
@@ -34,12 +34,15 @@ CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
34
34
  scope TEXT NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER
35
35
  );
36
36
  `);
37
+ try { db.exec('ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0'); } catch {}
37
38
 
38
- export function upsertUser({ uid, email = '', name = '' }) {
39
+ export const getUser = uid => db.prepare('SELECT * FROM users WHERE uid=?').get(uid);
40
+
41
+ export function upsertUser({ uid, email = '', name = '', emailVerified = false }) {
39
42
  const t = now();
40
- db.prepare(`INSERT INTO users(uid,email,name,created_at,last_login_at) VALUES(?,?,?,?,?)
41
- ON CONFLICT(uid) DO UPDATE SET email=excluded.email,name=excluded.name,last_login_at=excluded.last_login_at`)
42
- .run(uid, email, name, t, t);
43
+ db.prepare(`INSERT INTO users(uid,email,name,email_verified,created_at,last_login_at) VALUES(?,?,?,?,?,?)
44
+ ON CONFLICT(uid) DO UPDATE SET email=excluded.email,name=excluded.name,email_verified=excluded.email_verified,last_login_at=excluded.last_login_at`)
45
+ .run(uid, email, name, emailVerified ? 1 : 0, t, t);
43
46
  return db.prepare('SELECT * FROM users WHERE uid=?').get(uid);
44
47
  }
45
48
 
package/src/mcp.mjs CHANGED
@@ -57,7 +57,7 @@ function publicTool(tool) {
57
57
  readOnlyHint: tool.name === 'start_search' ? false : upstream.readOnlyHint === true,
58
58
  destructiveHint: destructive.has(tool.name),
59
59
  idempotentHint: upstream.idempotentHint === true,
60
- openWorldHint: tool.name === 'read_file' ? false : (openWorld.has(tool.name) || upstream.openWorldHint === true),
60
+ openWorldHint: openWorld.has(tool.name) || tool.name === 'read_file' || upstream.openWorldHint === true,
61
61
  },
62
62
  };
63
63
  }
package/src/oauth.mjs CHANGED
@@ -2,9 +2,9 @@ import crypto from 'node:crypto';
2
2
  import express from 'express';
3
3
  import { rateLimit } from 'express-rate-limit';
4
4
  import { config } from './config.mjs';
5
- import { verifyFirebaseToken, signMcpAccessToken } from './auth.mjs';
6
- import { createOauthCode, consumeOauthCode, getOauthClient, getRefreshToken, revokeRefreshToken, saveOauthClient, saveRefreshToken, upsertUser } from './db.mjs';
7
- import { normalizeScope, now, randomId, randomToken, sha256, validRedirectUri } from './util.mjs';
5
+ import { verifyFirebaseToken, signMcpAccessToken, verifyMcpAccessToken } from './auth.mjs';
6
+ import { createOauthCode, consumeOauthCode, getOauthClient, getRefreshToken, getUser, revokeRefreshToken, saveOauthClient, saveRefreshToken, upsertUser } from './db.mjs';
7
+ import { bearer, normalizeScope, now, randomId, randomToken, sha256, validRedirectUri } from './util.mjs';
8
8
 
9
9
  const CODE_TTL_MS = 5 * 60 * 1000;
10
10
  const router = express.Router();
@@ -18,12 +18,13 @@ function metadata() {
18
18
  authorization_endpoint: endpoint('/oauth/authorize'),
19
19
  token_endpoint: endpoint('/oauth/token'),
20
20
  registration_endpoint: endpoint('/oauth/register'),
21
+ userinfo_endpoint: endpoint('/oauth/userinfo'),
21
22
  revocation_endpoint: endpoint('/oauth/revoke'),
22
23
  response_types_supported: ['code'],
23
24
  grant_types_supported: ['authorization_code', 'refresh_token'],
24
25
  token_endpoint_auth_methods_supported: ['none'],
25
26
  code_challenge_methods_supported: ['S256'],
26
- scopes_supported: ['remcp:control', 'offline_access'],
27
+ scopes_supported: ['openid', 'email', 'remcp:control', 'offline_access'],
27
28
  };
28
29
  }
29
30
 
@@ -32,7 +33,7 @@ function protectedResource() {
32
33
  resource: endpoint('/mcp'),
33
34
  authorization_servers: [config.publicUrl],
34
35
  bearer_methods_supported: ['header'],
35
- scopes_supported: ['remcp:control', 'offline_access'],
36
+ scopes_supported: ['openid', 'email', 'remcp:control', 'offline_access'],
36
37
  resource_documentation: config.publicUrl,
37
38
  resource_policy_uri: endpoint('/privacy'),
38
39
  resource_tos_uri: endpoint('/terms'),
@@ -136,6 +137,24 @@ router.post('/oauth/token', tokenLimiter, async (req, res) => {
136
137
  }
137
138
  });
138
139
 
140
+
141
+ router.get('/oauth/userinfo', oauthLimiter, async (req, res) => {
142
+ noStore(res);
143
+ try {
144
+ const token = bearer(req);
145
+ if (!token) throw new Error('missing_token');
146
+ const auth = await verifyMcpAccessToken(token);
147
+ const scopes = new Set(auth.scope.split(/\s+/).filter(Boolean));
148
+ if (!scopes.has('openid') || !scopes.has('email')) throw new Error('insufficient_scope');
149
+ const user = getUser(auth.uid);
150
+ if (!user?.email || user.email_verified !== 1) throw new Error('verified_email_required');
151
+ res.json({ sub: auth.uid, email: user.email, email_verified: true, ...(user.name ? { name: user.name } : {}) });
152
+ } catch (error) {
153
+ res.setHeader('WWW-Authenticate', 'Bearer error="invalid_token"');
154
+ res.status(401).json({ error: 'invalid_token', error_description: error.message });
155
+ }
156
+ });
157
+
139
158
  router.post('/oauth/revoke', oauthLimiter, (req, res) => {
140
159
  const token = String(req.body?.token || '');
141
160
  if (token) revokeRefreshToken(sha256(token));
package/src/util.mjs CHANGED
@@ -9,7 +9,7 @@ export const bearer = req => {
9
9
  return value.startsWith('Bearer ') ? value.slice(7).trim() : '';
10
10
  };
11
11
  export function normalizeScope(value = '') {
12
- const allowed = new Set(['openid', 'offline_access', 'remcp:control']);
12
+ const allowed = new Set(['openid', 'email', 'offline_access', 'remcp:control']);
13
13
  const scopes = String(value).split(/\s+/).filter(Boolean).filter(scope => allowed.has(scope));
14
14
  if (!scopes.includes('remcp:control')) scopes.push('remcp:control');
15
15
  if (!scopes.includes('offline_access')) scopes.push('offline_access');
@@ -1,356 +0,0 @@
1
- {
2
- "$schema": "https://developers.openai.com/apps-sdk/schemas/chatgpt-app-submission.v1.json",
3
- "schema_version": 1,
4
- "app_info": {
5
- "display_name": "ReMCP",
6
- "subtitle": "Control your own computers",
7
- "description": "ReMCP lets authenticated users connect to computers they have paired and work with local files, directories, searches, terminal sessions, and processes through MCP. Each device uses a revocable credential and an outbound agent connection.",
8
- "category": "DEVELOPER_TOOLS"
9
- },
10
- "tools": {
11
- "list_devices": {
12
- "annotations": {
13
- "readOnlyHint": true,
14
- "openWorldHint": false,
15
- "destructiveHint": false
16
- },
17
- "justifications": {
18
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
19
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
20
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
21
- }
22
- },
23
- "get_config": {
24
- "annotations": {
25
- "readOnlyHint": true,
26
- "openWorldHint": false,
27
- "destructiveHint": false
28
- },
29
- "justifications": {
30
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
31
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
32
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
33
- }
34
- },
35
- "set_config_value": {
36
- "annotations": {
37
- "readOnlyHint": false,
38
- "openWorldHint": false,
39
- "destructiveHint": true
40
- },
41
- "justifications": {
42
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
43
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
44
- "destructive_justification": "Can change security and access configuration on the selected computer."
45
- }
46
- },
47
- "read_file": {
48
- "annotations": {
49
- "readOnlyHint": true,
50
- "openWorldHint": false,
51
- "destructiveHint": false
52
- },
53
- "justifications": {
54
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
55
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
56
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
57
- }
58
- },
59
- "read_multiple_files": {
60
- "annotations": {
61
- "readOnlyHint": true,
62
- "openWorldHint": false,
63
- "destructiveHint": false
64
- },
65
- "justifications": {
66
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
67
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
68
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
69
- }
70
- },
71
- "write_file": {
72
- "annotations": {
73
- "readOnlyHint": false,
74
- "openWorldHint": false,
75
- "destructiveHint": true
76
- },
77
- "justifications": {
78
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
79
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
80
- "destructive_justification": "Can overwrite or append file content on the selected computer."
81
- }
82
- },
83
- "write_pdf": {
84
- "annotations": {
85
- "readOnlyHint": false,
86
- "openWorldHint": false,
87
- "destructiveHint": true
88
- },
89
- "justifications": {
90
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
91
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
92
- "destructive_justification": "Can replace PDF content or remove pages when editing a PDF."
93
- }
94
- },
95
- "create_directory": {
96
- "annotations": {
97
- "readOnlyHint": false,
98
- "openWorldHint": false,
99
- "destructiveHint": false
100
- },
101
- "justifications": {
102
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
103
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
104
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
105
- }
106
- },
107
- "list_directory": {
108
- "annotations": {
109
- "readOnlyHint": true,
110
- "openWorldHint": false,
111
- "destructiveHint": false
112
- },
113
- "justifications": {
114
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
115
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
116
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
117
- }
118
- },
119
- "move_file": {
120
- "annotations": {
121
- "readOnlyHint": false,
122
- "openWorldHint": false,
123
- "destructiveHint": true
124
- },
125
- "justifications": {
126
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
127
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
128
- "destructive_justification": "Moves or renames filesystem objects and may replace an intended path."
129
- }
130
- },
131
- "start_search": {
132
- "annotations": {
133
- "readOnlyHint": false,
134
- "openWorldHint": false,
135
- "destructiveHint": false
136
- },
137
- "justifications": {
138
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
139
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
140
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
141
- }
142
- },
143
- "get_more_search_results": {
144
- "annotations": {
145
- "readOnlyHint": true,
146
- "openWorldHint": false,
147
- "destructiveHint": false
148
- },
149
- "justifications": {
150
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
151
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
152
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
153
- }
154
- },
155
- "stop_search": {
156
- "annotations": {
157
- "readOnlyHint": false,
158
- "openWorldHint": false,
159
- "destructiveHint": false
160
- },
161
- "justifications": {
162
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
163
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
164
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
165
- }
166
- },
167
- "list_searches": {
168
- "annotations": {
169
- "readOnlyHint": true,
170
- "openWorldHint": false,
171
- "destructiveHint": false
172
- },
173
- "justifications": {
174
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
175
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
176
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
177
- }
178
- },
179
- "get_file_info": {
180
- "annotations": {
181
- "readOnlyHint": true,
182
- "openWorldHint": false,
183
- "destructiveHint": false
184
- },
185
- "justifications": {
186
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
187
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
188
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
189
- }
190
- },
191
- "edit_block": {
192
- "annotations": {
193
- "readOnlyHint": false,
194
- "openWorldHint": false,
195
- "destructiveHint": true
196
- },
197
- "justifications": {
198
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
199
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
200
- "destructive_justification": "Changes existing file or document content on the selected computer."
201
- }
202
- },
203
- "start_process": {
204
- "annotations": {
205
- "readOnlyHint": false,
206
- "openWorldHint": true,
207
- "destructiveHint": true
208
- },
209
- "justifications": {
210
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
211
- "open_world_justification": "User-supplied process commands or input can contact external services or change external state.",
212
- "destructive_justification": "Can execute commands that modify files, processes, configuration, or external systems."
213
- }
214
- },
215
- "read_process_output": {
216
- "annotations": {
217
- "readOnlyHint": true,
218
- "openWorldHint": false,
219
- "destructiveHint": false
220
- },
221
- "justifications": {
222
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
223
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
224
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
225
- }
226
- },
227
- "interact_with_process": {
228
- "annotations": {
229
- "readOnlyHint": false,
230
- "openWorldHint": true,
231
- "destructiveHint": true
232
- },
233
- "justifications": {
234
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
235
- "open_world_justification": "User-supplied process commands or input can contact external services or change external state.",
236
- "destructive_justification": "Can send commands or input to a process that performs irreversible actions."
237
- }
238
- },
239
- "force_terminate": {
240
- "annotations": {
241
- "readOnlyHint": false,
242
- "openWorldHint": false,
243
- "destructiveHint": true
244
- },
245
- "justifications": {
246
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
247
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
248
- "destructive_justification": "Force-stops a running terminal session."
249
- }
250
- },
251
- "list_sessions": {
252
- "annotations": {
253
- "readOnlyHint": true,
254
- "openWorldHint": false,
255
- "destructiveHint": false
256
- },
257
- "justifications": {
258
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
259
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
260
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
261
- }
262
- },
263
- "list_processes": {
264
- "annotations": {
265
- "readOnlyHint": true,
266
- "openWorldHint": false,
267
- "destructiveHint": false
268
- },
269
- "justifications": {
270
- "read_only_justification": "Only retrieves local device data or results and does not change persistent user state.",
271
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
272
- "destructive_justification": "Does not delete, overwrite, revoke, terminate, or perform another irreversible action by itself."
273
- }
274
- },
275
- "kill_process": {
276
- "annotations": {
277
- "readOnlyHint": false,
278
- "openWorldHint": false,
279
- "destructiveHint": true
280
- },
281
- "justifications": {
282
- "read_only_justification": "Creates, changes, stops, or controls local device state or a local execution session.",
283
- "open_world_justification": "Operates on the paired computer or ReMCP session and does not itself publish or modify third-party internet state.",
284
- "destructive_justification": "Terminates the selected operating-system process."
285
- }
286
- }
287
- },
288
- "test_cases": [
289
- {
290
- "description": "List the computers available to the signed-in ReMCP account.",
291
- "user_prompt": "List my ReMCP devices and tell me which ones are online. Include the ReMCP Review Sandbox if it is available.",
292
- "file_attachment_urls": null,
293
- "tools_triggered": "list_devices",
294
- "expected_output": "Returns devices available to the authenticated account and includes the isolated ReMCP Review Sandbox with device id review-sandbox.",
295
- "expected_output_url": null
296
- },
297
- {
298
- "description": "Browse a directory on a paired review computer.",
299
- "user_prompt": "Using ReMCP device id review-sandbox, list the files in /workspace.",
300
- "file_attachment_urls": null,
301
- "tools_triggered": "list_directory",
302
- "expected_output": "Returns the isolated sandbox directory entries without changing them.",
303
- "expected_output_url": null
304
- },
305
- {
306
- "description": "Read a text file from a paired review computer.",
307
- "user_prompt": "Using ReMCP device id review-sandbox, read /workspace/README.txt and summarize it.",
308
- "file_attachment_urls": null,
309
- "tools_triggered": "read_file",
310
- "expected_output": "Returns the seeded review-sandbox README content so it can be summarized.",
311
- "expected_output_url": null
312
- },
313
- {
314
- "description": "Create a new file on a paired review computer.",
315
- "user_prompt": "Using ReMCP device id review-sandbox, create /workspace/review-note.txt containing exactly: ReMCP submission test.",
316
- "file_attachment_urls": null,
317
- "tools_triggered": "write_file",
318
- "expected_output": "Creates the requested file only inside the authenticated reviewer sandbox and reports the write result.",
319
- "expected_output_url": null
320
- },
321
- {
322
- "description": "Run a harmless terminal command on a paired review computer.",
323
- "user_prompt": "Using ReMCP device id review-sandbox, run pwd and show me the result.",
324
- "file_attachment_urls": null,
325
- "tools_triggered": "start_process",
326
- "expected_output": "Runs the sandbox-safe command and returns /workspace; arbitrary terminal commands remain blocked in the review sandbox.",
327
- "expected_output_url": null
328
- }
329
- ],
330
- "negative_test_cases": [
331
- {
332
- "description": "Do not invoke ReMCP for general weather information.",
333
- "user_prompt": "What will the weather be in Warsaw tomorrow?",
334
- "file_attachment_urls": null,
335
- "tools_triggered": null,
336
- "expected_output": "ReMCP should not be invoked because the request does not involve a paired computer.",
337
- "expected_output_url": null
338
- },
339
- {
340
- "description": "Do not invoke ReMCP for standalone writing help.",
341
- "user_prompt": "Draft a polite email asking my landlord to repair a leaking faucet.",
342
- "file_attachment_urls": null,
343
- "tools_triggered": null,
344
- "expected_output": "ReMCP should not be invoked because no local computer action or data is needed.",
345
- "expected_output_url": null
346
- },
347
- {
348
- "description": "Do not invoke ReMCP for a conceptual technical explanation.",
349
- "user_prompt": "Explain OAuth PKCE in simple terms.",
350
- "file_attachment_urls": null,
351
- "tools_triggered": null,
352
- "expected_output": "ReMCP should not be invoked because the user only requested an explanation.",
353
- "expected_output_url": null
354
- }
355
- ]
356
- }