A personal Model Context Protocol (MCP) server built in Node.js/TypeScript. Exposes Google Calendar, Google Tasks, and Weather data as MCP tools for use by LLM clients.
- Google Calendar (readonly) — list calendars, list/get/search events
- Google Tasks (readonly) — list task lists, list/get tasks
- Weather — current conditions for any location via Google Weather + Geocoding APIs
- API key authentication — ID + secret pair for all endpoints
- OAuth token management — automatic access token refresh with Postgres persistence
- Idempotent migrations — SQL migration scripts run on startup
- Node.js ≥ 22
- PostgreSQL
- Google Cloud project with:
- Calendar API enabled
- Tasks API enabled
- Weather API enabled
- Geocoding API enabled
- OAuth 2.0 client credentials (for Calendar/Tasks)
- API key (for Weather/Geocoding)
npm installcp .env.example .envEdit .env with your database connection string and port:
DATABASE_URL=postgresql://user:pass@localhost:5432/connect_mcp
PORT=3000
CONFIG_PATH=config.json
OAUTH_REDIRECT_BASE_URL=http://localhost:3000
OPENOBSERVE_URL=http://localhost:5080/api/default/connect-mcp/_json
cp config.example.json config.jsonEdit config.json with your provider credentials and API keys:
{
"providers": {
"google": {
"apiKey": "your-google-api-key",
"oauthClientId": "your-google-oauth-client-id",
"oauthClientSecret": "your-google-oauth-client-secret"
}
},
"apiKeys": [
{
"id": "uuid-for-key",
"name": "my-key",
"secret": "your-api-key-secret"
}
]
}Each API key has its own OAuth tokens — use the same key pair for both OAuth setup and MCP access.
npm run build
npm startOr for development with hot-reload:
npm run devOpen the OAuth setup URL in a browser to authorise Google Calendar and Tasks access ():
http://localhost:3000/auth/google?apiKeyId=your-api-key-id&apiKeySecret=your-api-key-secret
The browser will redirect to Google's consent screen. After authorising, tokens are stored in the database automatically, linked to the API key ID used.
Add to your settings:
{
"mcp": {
"servers": {
"connect-mcp": {
"type": "http",
"url": "http://localhost:3000/mcp?apiKeyId=your-api-key-id&apiKeySecret=your-api-key-secret"
}
}
}
}Note: SSE is not yet tested. It was introduced as it was suspected that a client was using legacy protocols, but this has turned out not to be the case.
Clients that use the older SSE transport (protocol version 2024-11-05) should connect to /sse instead of /mcp:
{
"connect-mcp": {
"url": "http://localhost:3000/sse?apiKeyId=your-api-key-id&apiKeySecret=your-api-key-secret"
}
}The SSE endpoint works as follows:
GET /sse— establishes the SSE stream and returns aPOSTendpoint URL- The client sends JSON-RPC messages to the returned endpoint (
/sse/messages) - Auth credentials from the initial
GETare forwarded to the messages endpoint automatically
The apiKeyId and apiKeySecret query parameters authenticate the connection. OAuth tokens are looked up automatically from the key's ID — the LLM never needs to handle credentials.
The original MCP protocol used Server-Sent Events (SSE) for streaming and a separate POST endpoint for client messages. This pattern is now considered legacy and is only required for backward compatibility with older clients. The MCP SDK and spec have moved to the Streamable HTTP protocol, which is more robust, easier to implement, and better supported by modern tools.
SSE is deprecated in the MCP SDK and will be removed in future releases. New clients should use the Streamable HTTP transport (/mcp endpoint) instead of SSE (/sse and /sse/messages).
| Feature | Streamable HTTP (/mcp) |
Legacy SSE (/sse, /sse/messages) |
|---|---|---|
| Connection handshake | Single POST (initialize) | GET SSE stream, then POST endpoint |
| Streaming | Bidirectional over HTTP | Server-to-client via SSE, client-to-server via POST |
| Session management | Session ID in headers | Session ID in query param |
| Authentication | API key in query or headers | API key in query or headers |
| Client compatibility | VS Code, modern MCP clients | Legacy, older MCP clients |
| Status | Current standard | Legacy, deprecated |
Summary:
- Use
/mcpfor all new clients and integrations. - Use
/sseonly if your client does not support Streamable HTTP (e.g. legacy clients). - Both endpoints require API key authentication and are equally secure.
For more details, see Model Context Protocol documentation.
Pre-built request files for the REST Client VS Code extension are in the requests/ directory.
- Install the REST Client extension (
humao.rest-client) - Add the following variables to your
.envfile (alongside the server config vars):
REST_API_KEY_ID=your-api-key-uuid
REST_API_KEY_SECRET=your-api-key-secret
The base URL (http://localhost:3000) is configured in .vscode/settings.json and is safe to commit. The API key credentials are read from .env via {{$dotenv ...}} and never appear in version control.
| File | Contents |
|---|---|
requests/health.http |
Health check |
requests/auth.http |
Initiate Google OAuth (open URL in browser) |
requests/mcp-calendar.http |
Calendar tools — list, get, search |
requests/mcp-tasks.http |
Tasks tools — list task lists, list tasks, get task |
requests/mcp-weather.http |
Weather tool |
Open any .http file. A Send Request CodeLens link appears above each ### request block — click it to send. The response opens in a split pane. Note if a link does not appear, place the cursor in the request body or on the first line and Ctrl + P > Rest Client / Send Request.
For MCP tool requests, always send the Initialize MCP session request first within that file. The @sessionId variable is captured automatically from the response header and used by all subsequent requests in the same file.
The Docker image does not include config.json or .env (both are in .dockerignore). In production, these are supplied by your orchestration:
| File | Source | Mount |
|---|---|---|
config.json |
SOPS-encrypted Secret (connect-mcp-config-file) |
Volume mount at /app/config.json |
No .env file is used in production — all environment variables (DATABASE_URL, PORT, LOG_LEVEL, CONFIG_PATH, OAUTH_REDIRECT_BASE_URL, OPENOBSERVE_URL) are injected directly by Kubernetes.
Every request must include valid API key credentials (both ID and secret), either as:
- Headers:
x-api-key-id: your-key-id+x-api-key-secret: your-key-secret - Query parameters:
?apiKeyId=your-key-id&apiKeySecret=your-key-secret
The API key ID is used to look up OAuth tokens for Google Calendar/Tasks requests. No separate user credentials are needed.
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/auth/:provider |
GET | Initiate OAuth flow (requires API key) |
/auth/:provider/callback |
GET | OAuth callback (from Google) |
/mcp |
POST/GET/DELETE | MCP Streamable HTTP transport (requires API key) |
/sse |
GET | Legacy SSE transport — establishes SSE stream (requires API key) |
/sse/messages |
POST | Legacy SSE transport — receives JSON-RPC messages |
| Tool | Description | Key Parameters |
|---|---|---|
list_calendars |
List all Google Calendars | — |
list_calendar_events |
List events from a calendar | calendarId, timeMin, timeMax, maxResults |
get_calendar_event |
Get a single event | calendarId, eventId |
search_calendar_events |
Search events by query | calendarId, query, timeMin, timeMax |
list_task_lists |
List all Google Task Lists | — |
list_tasks |
List tasks in a task list | taskListId, showCompleted, dueMin, dueMax |
get_task |
Get a single task | taskListId, taskId |
get_weather |
Get current weather | location |
├── src/
│ ├── index.ts # Entry point — Express + MCP server bootstrap
│ ├── config/
│ │ ├── env.ts # Environment variable config (zod validated)
│ │ ├── loader.ts # JSON config file loader
│ │ └── schema.ts # Zod schemas for API keys, providers
│ ├── db/
│ │ ├── database.ts # Kysely database client + type definitions
│ │ └── migrator.ts # Migration runner (reads migrations/ directory)
│ ├── mcp/
│ │ └── server.ts # MCP server creation + tool registration
│ ├── middleware/
│ │ ├── auth.ts # API key authentication
│ │ └── logging.ts # Request logging + global error handler
│ ├── routes/
│ │ └── auth.ts # OAuth setup + callback endpoints
│ ├── services/
│ │ └── token-service.ts # OAuth token fetch + refresh logic
│ └── tools/
│ ├── calendar.ts # Google Calendar MCP tools
│ ├── tasks.ts # Google Tasks MCP tools
│ └── weather.ts # Weather MCP tool
├── migrations/
│ ├── 0001_create_oauth_token.sql
│ └── 0002_rename_user_uuid_to_api_key_id.sql
├── requests/
│ ├── health.http # Health check
│ ├── auth.http # OAuth initiation
│ ├── mcp-calendar.http # Calendar tool requests
│ ├── mcp-tasks.http # Tasks tool requests
│ └── mcp-weather.http # Weather tool requests
├── config.example.json
├── .env.example
├── tsconfig.json
└── package.json
Uses PostgreSQL with Kysely (lightweight type-safe query builder).
- Snake case for all DB identifiers
- Index suffix:
_idx(e.g.oauth_token_api_key_id_idx) - Foreign key suffix:
_fkey - Unique constraint suffix:
_key(e.g.oauth_token_api_key_id_provider_key) - Timestamp columns:
_utcsuffix (e.g.created_utc,updated_utc)
SQL migration scripts in migrations/ are named 0000_description.sql and run automatically on startup. All migrations are idempotent.
Development secrets are managed using SOPS with age encryption. Encrypted .env.enc files are committed to the repository. Plaintext .env files are never committed.
Production secrets are managed in the appsoftware-infra project, and are also managed via SOPS using a different process.
| Plaintext | Encrypted (committed) | Purpose |
|---|---|---|
src/.env |
src/.env.enc |
Docker Compose secrets |
src/AppBase.Web/.env |
src/AppBase.Web/.env.enc |
Application development secrets (loaded by DotNetEnv in Program.cs) |
Install SOPS and age (once per machine):
winget install Mozilla.SOPS
winget install FiloSottile.age- Retrieve the age key from the password safe entry "SOPS age public / private encryption key (appsoftware-developer)".
- Place the key file at
%AppData%\sops\age\keys.txt(create thesops\agedirectory if it doesn't exist). - Decrypt secrets:
npm run setup| Script | Description |
|---|---|
npm run decrypt |
Decrypt all .env.enc → .env (overwrites existing plaintext files) |
npm run encrypt |
Encrypt all .env → .env.enc (after editing secrets) |
npm run setup |
Alias for decrypt |
To change a secret value, decrypt to .env, edit it, then re-encrypt:
npm run decrypt
# edit the relevant .env file
npm run encrypt
Alternatively, open a single encrypted file in your editor with SOPS (it will re-encrypt on save):
sops --input-type dotenv --output-type dotenv src/AppBase.Web/.env.enc
.env.encfiles are encrypted with SOPS using age. Secret keys are visible in plaintext; only values are encrypted. These files are committed to source control..envfiles are the decrypted plaintext versions used at runtime. They are git-ignored and never committed..sops.yamlat the repository root defines the age public key used for encryption.package.jsonat the repository root contains theencryptanddecryptnpm scripts that invokesopsdirectly for each managed file.
To bring a new .env file under SOPS management - add corresponding sops commands for it to the decrypt and encrypt scripts in package.json. Also check .sops.yaml - though it currently matches .env files at all paths under this project.