Проверить окружение
node --version
npm --version
codex --versionКак подключить Google-аккаунт к Codex на VPS: создать OAuth-приложение, получить token.json, включить Gmail, Calendar и Drive API, добавить локальный MCP-сервер и работать с почтой, календарем и файлами из терминала.
Codex будет видеть Google Workspace как набор инструментов. В базовой версии доступ только на чтение, чтобы безопасно искать письма, события и документы.
На сервере нужны Node.js, npm и Codex CLI. Секреты хранятся отдельно от репозитория.
node --version
npm --version
codex --versionmkdir -p /root/codexclaw/.secrets/google
mkdir -p /root/codexclaw/workspace/tools/google-mcp
chmod 700 /root/codexclaw/.secrets
chmod 700 /root/codexclaw/.secrets/google/root/codexclaw/.secrets/google/client_secret.json
/root/codexclaw/.secrets/google/token.json
/root/codexclaw/.secrets/google/oauth_state.txt
/root/codexclaw/workspace/tools/google-mcp/server.mjsСначала создается Google Cloud project, затем включаются API и настраивается экран согласия.
Откройте Google Cloud Console, создайте проект или выберите существующий, запомните Project number и Project ID.
В Library включите Gmail API, Google Calendar API и Google Drive API. Если API не включить, smoke test вернет ошибку API disabled.
В Google Auth Platform заполните app name, support email и developer contact email. Для теста оставьте Publishing status: Testing.
В Audience добавьте Google-аккаунт, который подключаете. Без этого будет 403 access_denied.
Для ручного терминального сценария используется localhost redirect. Сервер на localhost запускать не нужно.
Application type: Web application
Authorized redirect URI: http://localhost:3000/oauth2callbacknano /root/codexclaw/.secrets/google/client_secret.json
chmod 600 /root/codexclaw/.secrets/google/client_secret.jsonClient secret и token.json нельзя отправлять в чат, хранить в публичной базе знаний или коммитить в git.
Команда ниже печатает OAuth URL и сохраняет state. Ссылку нужно открыть в браузере под тем Google-аккаунтом, который подключаете.
node - <<'NODE'
const fs = require('fs');
const crypto = require('crypto');
const path = '/root/codexclaw/.secrets/google/client_secret.json';
const secret = JSON.parse(fs.readFileSync(path, 'utf8'));
const cfg = secret.web || secret.installed;
const state = crypto.randomBytes(24).toString('hex');
fs.writeFileSync('/root/codexclaw/.secrets/google/oauth_state.txt', state, { mode: 0o600 });
const params = new URLSearchParams({
client_id: cfg.client_id,
redirect_uri: 'http://localhost:3000/oauth2callback',
response_type: 'code',
scope: [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/drive.metadata.readonly',
'https://www.googleapis.com/auth/drive.readonly'
].join(' '),
access_type: 'offline',
prompt: 'consent',
include_granted_scopes: 'true',
state,
});
console.log('https://accounts.google.com/o/oauth2/v2/auth?' + params.toString());
NODEВставьте полный callback URL в переменную CALLBACK_URL и выполните обмен на сервере.
CALLBACK_URL='http://localhost:3000/oauth2callback?state=...&code=...&scope=...'
node - <<'NODE'
const fs = require('fs');
const callbackUrl = process.env.CALLBACK_URL;
if (!callbackUrl) throw new Error('CALLBACK_URL is required');
const secretPath = '/root/codexclaw/.secrets/google/client_secret.json';
const tokenPath = '/root/codexclaw/.secrets/google/token.json';
const statePath = '/root/codexclaw/.secrets/google/oauth_state.txt';
const secret = JSON.parse(fs.readFileSync(secretPath, 'utf8'));
const cfg = secret.web || secret.installed;
const url = new URL(callbackUrl);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const expectedState = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8').trim() : '';
if (!code) throw new Error('No code in callback URL');
if (expectedState && state !== expectedState) throw new Error('OAuth state mismatch');
const body = new URLSearchParams({
client_id: cfg.client_id,
client_secret: cfg.client_secret,
code,
grant_type: 'authorization_code',
redirect_uri: 'http://localhost:3000/oauth2callback',
});
fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
.then(async (res) => {
const data = await res.json();
if (!res.ok) throw new Error(JSON.stringify(data));
data.expires_at = Date.now() + (data.expires_in || 3600) * 1000;
fs.writeFileSync(tokenPath, JSON.stringify(data, null, 2), { mode: 0o600 });
fs.chmodSync(tokenPath, 0o600);
console.log('token saved:', tokenPath);
console.log('has access_token:', Boolean(data.access_token));
console.log('has refresh_token:', Boolean(data.refresh_token));
})
.catch((err) => {
console.error(err.message);
process.exit(1);
});
NODEДо подключения к Codex проверьте, что токен и API работают напрямую.
node - <<'NODE'
const fs = require('fs');
const token = JSON.parse(fs.readFileSync('/root/codexclaw/.secrets/google/token.json', 'utf8'));
fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=1', {
headers: { Authorization: `Bearer ${token.access_token}` }
}).then(r => r.json().then(j => console.log(JSON.stringify({ ok: r.ok, status: r.status, j }, null, 2))));
NODEnode - <<'NODE'
const fs = require('fs');
const token = JSON.parse(fs.readFileSync('/root/codexclaw/.secrets/google/token.json', 'utf8'));
fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList?maxResults=5', {
headers: { Authorization: `Bearer ${token.access_token}` }
}).then(r => r.json().then(j => console.log(JSON.stringify({ ok: r.ok, status: r.status, count: j.items?.length || 0 }, null, 2))));
NODEnode - <<'NODE'
const fs = require('fs');
const token = JSON.parse(fs.readFileSync('/root/codexclaw/.secrets/google/token.json', 'utf8'));
fetch('https://www.googleapis.com/drive/v3/files?pageSize=5&fields=files(id,name,mimeType,modifiedTime)', {
headers: { Authorization: `Bearer ${token.access_token}` }
}).then(r => r.json().then(j => console.log(JSON.stringify({ ok: r.ok, status: r.status, count: j.files?.length || 0 }, null, 2))));
NODEMCP-сервер кладется в рабочую папку и добавляется в .codex/config.toml.
nano /root/codexclaw/workspace/tools/google-mcp/server.mjs
chmod +x /root/codexclaw/workspace/tools/google-mcp/server.mjs[mcp_servers.google_workspace]
command = "node"
args = ["/root/codexclaw/workspace/tools/google-mcp/server.mjs"]cd /root/codexclaw
codex mcp list
codexНачинайте с чтения и поиска. Полное тело письма открывайте только по выбранному id.
Проверь, видишь ли ты Google Workspace MCP tools. Не выводи содержимое писем, только список инструментов.Найди в Gmail последние 5 писем от example@example.com, покажи только отправителя, тему и дату.Посмотри мой календарь на сегодня, покажи время, название и ссылку на встречу, если есть.Найди на Google Drive файлы по слову "ДОМ.РФ", покажи названия и ссылки.Большинство проблем связано с test users, disabled API или отсутствием refresh_token.
Для другого Google-аккаунта не переносите старый token.json. Оставьте server.mjs и config, но получите новый token.json через OAuth.
Следующая инструкция: Яндекс.Почта, Диск и Календарь для Codex