Skip to content

appsoftwareltd/connect-mcp-demo

Repository files navigation

Connect MCP Server

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.

Features

  • 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

Quick Start

Prerequisites

  • 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)

1. Install dependencies

npm install

2. Create environment file

cp .env.example .env

Edit .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

3. Create config file

cp config.example.json config.json

Edit 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.

4. Build and start

npm run build
npm start

Or for development with hot-reload:

npm run dev

5. Set up Google OAuth

Open 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.

6. Connect an MCP client

VS Code (Streamable HTTP)

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"
      }
    }
  }
}

SSE clients (Legacy SSE)

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:

  1. GET /sse — establishes the SSE stream and returns a POST endpoint URL
  2. The client sends JSON-RPC messages to the returned endpoint (/sse/messages)
  3. Auth credentials from the initial GET are 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.

MCP Transport Protocols: Streamable HTTP vs. Legacy SSE

Why SSE is Legacy

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).

Protocol Differences

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 /mcp for all new clients and integrations.
  • Use /sse only 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.

Testing with REST Client

Pre-built request files for the REST Client VS Code extension are in the requests/ directory.

Setup

  1. Install the REST Client extension (humao.rest-client)
  2. Add the following variables to your .env file (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.

Request files

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

Usage

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.


Deployment (Docker Based)

Runtime File Mapping

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.


API

Authentication

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.

Endpoints

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

MCP Tools

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

Project Structure

├── 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

Database

Uses PostgreSQL with Kysely (lightweight type-safe query builder).

Conventions

  • 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: _utc suffix (e.g. created_utc, updated_utc)

Migrations

SQL migration scripts in migrations/ are named 0000_description.sql and run automatically on startup. All migrations are idempotent.

Secrets Management (SOPS + age)

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.

Managed files

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)

Prerequisites

Install SOPS and age (once per machine):

winget install Mozilla.SOPS
winget install FiloSottile.age

First-time setup

  1. Retrieve the age key from the password safe entry "SOPS age public / private encryption key (appsoftware-developer)".
  2. Place the key file at %AppData%\sops\age\keys.txt (create the sops\age directory if it doesn't exist).
  3. Decrypt secrets:
npm run setup

Available scripts

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

Editing secrets

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

How it works

  • .env.enc files are encrypted with SOPS using age. Secret keys are visible in plaintext; only values are encrypted. These files are committed to source control.
  • .env files are the decrypted plaintext versions used at runtime. They are git-ignored and never committed.
  • .sops.yaml at the repository root defines the age public key used for encryption.
  • package.json at the repository root contains the encrypt and decrypt npm scripts that invoke sops directly for each managed file.

Adding a new .env 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.

About

A demonstration of how to implement a lightweight remote MCP server

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors