Skip to main content
Glama
aliasdhacker

WorkPulse MCP Server

by aliasdhacker

WorkPulse MCP Server

A Model Context Protocol server that lets Claude (Desktop, Code, or any MCP client) enter and query expenses, invoices, and contracts in a WorkPulse instance through its REST API.

  • 14 tools across expenses, invoices, contracts, lookups, batches, and OCR document processing

  • Batch rollback — group bulk inserts and undo them in one call

  • Two transportsstdio for local desktop use (default), or Streamable HTTP protected by OAuth bearer tokens for network deployment

  • Credentials stay server-side — WorkPulse username/password live only in the server's environment; the model and MCP client never see them

  • Optional OCR pipeline — turn bank statements, receipts and bills (PDF/images) into expenses via a DriftMoney DocTR + Ollama pipeline

Claude / MCP client  ⇄  WorkPulse MCP server  ⇄  WorkPulse REST API
                                  ⇣ (optional)
                            OCR pipeline

Contents


Related MCP server: life-agent-mcp

Quick start

Requirements: Python 3.10+ and a running WorkPulse server.

git clone https://github.com/aliasdhacker/workpulse-mcp.git
cd workpulse-mcp
python -m venv .venv
# Windows: .venv\Scripts\activate    macOS/Linux: source .venv/bin/activate
pip install -e .

Set the WorkPulse credentials (these are read by the server process only):

export WORKPULSE_API_URL=http://localhost:8080
export WORKPULSE_USERNAME=<your-username>
export WORKPULSE_PASSWORD=<your-password>

Run it:

workpulse-mcp                # stdio, for Claude Desktop / Claude Code
# or
python -m workpulse_mcp.server

Then add it to your client — see Client configuration. Copy .env.example to .env if you prefer a file; it is git-ignored.


Architecture

┌────────────────────────────────┐
│ Claude Desktop / Claude Code / │
│ any MCP client                 │
└──────────────┬─────────────────┘
               │  MCP JSON-RPC
               │  stdio  ─or─  Streamable HTTP (+ OAuth bearer token)
               ▼
┌────────────────────────────────────────────────────────────┐
│  WorkPulse MCP server  (Python, mcp SDK 2.x)               │
│                                                            │
│  http_auth.py   bearer-token verifier for HTTP transports  │
│  server.py      MCPServer + lifespan (logs in to WorkPulse)│
│  auth.py        WorkPulse JWT login / refresh / re-login   │
│  client.py      httpx client w/ bearer + retry/backoff     │
│  batch.py       batch tracker  → ~/.workpulse-mcp/batches.json
│  tools/         expenses · invoices · contracts · lookups  │
│                 batches · receipts                         │
└──────────────┬──────────────────────────┬──────────────────┘
               │ HTTPS + WorkPulse JWT     │ HTTP (optional)
               ▼                           ▼
┌──────────────────────────┐   ┌────────────────────────────┐
│  WorkPulse REST API      │   │  DriftMoney OCR pipeline   │
│  /api/expenses …         │   │  POST /parse               │
│  /api/auth/login|refresh │   │  (DocTR + Ollama)          │
└──────────────────────────┘   └────────────────────────────┘

Every tool call follows the same path: the tool calls auth.ensure_authenticated() (refreshing or re-logging-in as needed), then makes one or more REST calls through client.py, and — for create_* tools — records the new entity's ID in the active batch so it can be rolled back.


Credential model

There are two independent credential domains, and they never cross.

Domain

Who holds it

How it is used

WorkPulse login (WORKPULSE_USERNAME / WORKPULSE_PASSWORD)

The MCP server process only, via environment variables

Exchanged at startup for a WorkPulse JWT (POST /api/auth/login). The JWT is kept in memory, sent as Authorization: Bearer on every REST call, refreshed via POST /api/auth/refresh 30 s before expiry, and re-obtained with a full login if refresh fails.

MCP endpoint access (OAuth bearer token)

The MCP client (Claude)

Only for the HTTP transports. Presented to this server, which validates it and, if valid, serves the request using its own WorkPulse session.

Consequences:

  • The model never sees, and cannot request, the WorkPulse password or JWT. No tool exposes them; they are not in any tool output.

  • Anyone who can reach the HTTP endpoint with a valid bearer token acts as the configured WorkPulse user. Scope your WorkPulse account accordingly.

  • In stdio mode there is no network endpoint; the client launches the server as a child process and passes the env vars in its config (see below). Keep that config file private.


Transports

Select with MCP_TRANSPORT. stdio is the default.

stdio (default)

MCP_TRANSPORT=stdio workpulse-mcp

Standard for Claude Desktop and Claude Code. All logging goes to stderr; stdout is the JSON-RPC channel.

Streamable HTTP + OAuth

MCP_TRANSPORT=streamable-http MCP_HOST=0.0.0.0 MCP_PORT=8888 workpulse-mcp
# endpoint: http://<host>:8888/mcp

The HTTP endpoint is always protected; the server refuses to start unless an auth mode is configured (or MCP_AUTH_MODE=none is set explicitly). Auth is implemented with the mcp SDK's built-in resource-server support: a TokenVerifier is passed to MCPServer(token_verifier=…, auth=AuthSettings(…)), and the SDK's BearerAuthBackend / RequireAuthMiddleware wrap the /mcp route and publish RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource/mcp, per the MCP authorization spec.

Unauthenticated requests get 401 with a WWW-Authenticate: Bearer … resource_metadata="…" header so spec-compliant clients can discover the authorization server; tokens missing a required scope get 403 insufficient_scope.

Three verifier modes (src/workpulse_mcp/http_auth.py):

Mode

MCP_AUTH_MODE

When to use

Required env

Static token

static (auto if MCP_BEARER_TOKEN is set)

Local development, a single trusted client

MCP_BEARER_TOKEN (≥16 chars)

JWT (JWKS)

jwt (auto if MCP_OAUTH_ISSUER is set)

Production with an OAuth 2.1 / OIDC authorization server issuing signed JWT access tokens

MCP_OAUTH_ISSUER, MCP_RESOURCE_URL (and optionally MCP_OAUTH_JWKS_URL, MCP_OAUTH_AUDIENCE, MCP_OAUTH_REQUIRED_SCOPES)

Introspection

introspection (auto if MCP_OAUTH_INTROSPECTION_URL is set)

Production with opaque tokens (RFC 7662)

MCP_OAUTH_ISSUER, MCP_OAUTH_INTROSPECTION_URL, MCP_RESOURCE_URL, optionally MCP_OAUTH_CLIENT_ID / MCP_OAUTH_CLIENT_SECRET

None

none (never auto)

Isolated lab networks only

Development (static token):

export MCP_TRANSPORT=streamable-http
export MCP_BEARER_TOKEN="$(python -c 'import secrets;print(secrets.token_urlsafe(32))')"
export WORKPULSE_USERNAME=<your-username> WORKPULSE_PASSWORD=<your-password>
workpulse-mcp

Clients send Authorization: Bearer <that token> (see examples/mcp.http.json.example).

Production (JWT):

export MCP_TRANSPORT=streamable-http
export MCP_HOST=0.0.0.0 MCP_PORT=8888
export MCP_RESOURCE_URL=https://mcp.example.com/mcp      # public URL of this endpoint = expected `aud`
export MCP_OAUTH_ISSUER=https://auth.example.com          # your authorization server
export MCP_OAUTH_REQUIRED_SCOPES=workpulse:write          # optional
workpulse-mcp

The verifier discovers jwks_uri from the issuer's /.well-known/oauth-authorization-server (or /.well-known/openid-configuration), caches keys, and validates signature, iss, aud, and exp with PyJWT. Put TLS termination (nginx, Caddy, a cloud load balancer) in front of it; the server itself speaks plain HTTP.

Register https://mcp.example.com/mcp as a resource / audience in your authorization server and let clients obtain tokens for it through the normal OAuth 2.1 + PKCE flow. Because this server publishes protected-resource metadata, MCP clients that implement the authorization spec (Claude's remote-MCP connectors, MCP Inspector, etc.) find your authorization server automatically from the 401.

sse is also accepted for legacy clients and is protected identically.


Client configuration

Claude Code (.mcp.json in your project, or claude mcp add)

{
  "mcpServers": {
    "workpulse": {
      "type": "stdio",
      "command": "workpulse-mcp",
      "env": {
        "WORKPULSE_API_URL": "http://localhost:8080",
        "WORKPULSE_USERNAME": "<your-username>",
        "WORKPULSE_PASSWORD": "<your-password>",
        "OCR_PIPELINE_URL": "http://localhost:8000"
      }
    }
  }
}

Remote server over Streamable HTTP:

{
  "mcpServers": {
    "workpulse": {
      "type": "http",
      "url": "http://localhost:8888/mcp",
      "headers": { "Authorization": "Bearer <your-mcp-bearer-token>" }
    }
  }
}

Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "workpulse": {
      "command": "python",
      "args": ["-m", "workpulse_mcp.server"],
      "env": {
        "WORKPULSE_API_URL": "http://localhost:8080",
        "WORKPULSE_USERNAME": "<your-username>",
        "WORKPULSE_PASSWORD": "<your-password>",
        "OCR_PIPELINE_URL": "http://localhost:8000"
      }
    }
  }
}

If python on your PATH is not the interpreter where you installed the package, use the venv's interpreter (.../.venv/bin/python or ...\.venv\Scripts\python.exe) as command.

Ready-to-copy versions live in examples/.


Tools

All tools return JSON text. IDs referenced below come from the lookup tools.

Expenses

create_expense

Parameter

Type

Required

Description

date

string

yes

YYYY-MM-DD

amount

float

yes

Positive dollar amount

category

string

yes

SUPPLIES, SOFTWARE, TRAVEL, EQUIPMENT, SUBCONTRACTOR, OTHER

vendor

string

no

Merchant name

description

string

no

What was purchased

receipt_reference

string

no

Receipt number or file reference

project_id

int

no

Associate with a project

tax_category

string

no

DEDUCTIBLE (default), PARTIALLY_DEDUCTIBLE, NON_DEDUCTIBLE

batch_id

string

no

Batch to track this in (uses active batch if omitted)

Create an expense for $127.50 at Office Depot on 2026-02-20 for supplies, tax deductible

list_expenses

Parameter

Type

Required

Description

start

string

no

Start date YYYY-MM-DD

end

string

no

End date YYYY-MM-DD

category

string

no

Filter by category

Show me all travel expenses from January 2026

Invoices

create_invoice

Parameter

Type

Required

Description

client_id

int

yes

Client ID (use list_clients to find)

invoice_number

string

yes

e.g. "INV-2026-003"

invoice_date

string

yes

YYYY-MM-DD

subtotal

float

no

Subtotal before adjustments

total

float

no

Total after adjustments/discounts

status

string

no

DRAFT (default), SENT, PAID

period_start

string

no

Billing period start YYYY-MM-DD

period_end

string

no

Billing period end YYYY-MM-DD

adjustment

float

no

Adjustment amount (can be negative)

adjustment_note

string

no

Reason for adjustment

discount_amount

float

no

Fixed discount

discount_percent

float

no

Percentage discount (0-100)

discount_reason

string

no

Reason for discount

due_date

string

no

Payment due date YYYY-MM-DD

notes

string

no

Additional notes

batch_id

string

no

Batch for rollback tracking

Create a draft invoice INV-2026-003 for client Acme Corp dated today, subtotal $3200, total $3200, for the period Feb 1-28

generate_invoice

Generates an invoice from billable time entries; the API pulls matching entries and creates line items.

Parameter

Type

Required

Description

client_id

int

yes

Client ID

period_start

string

yes

YYYY-MM-DD

period_end

string

yes

YYYY-MM-DD

invoice_number

string

no

Override invoice number

invoice_date

string

no

Override invoice date

adjustment

float

no

Adjustment amount

adjustment_note

string

no

Reason for adjustment

notes

string

no

Additional notes

batch_id

string

no

Batch for rollback tracking

Generate an invoice for Acme Corp covering January 2026

list_invoices

Parameter

Type

Required

Description

client_id

int

no

Filter by client

status

string

no

DRAFT, SENT, PAID

Show me all unpaid invoices for client 3

Contracts

create_contract

Parameter

Type

Required

Description

client_id

int

yes

Client ID

title

string

yes

Contract title

contract_type

string

no

HOURLY (default), FIXED_PRICE, RETAINER

status

string

no

DRAFT (default), SENT, ACTIVE, COMPLETED, EXPIRED, TERMINATED

description

string

no

Contract description

terms

string

no

Payment terms

scope

string

no

Scope of work

hourly_rate

float

no

Rate for HOURLY contracts

estimated_hours

float

no

Estimated hours

estimated_amount

float

no

Estimated total for FIXED_PRICE

start_date

string

no

YYYY-MM-DD

end_date

string

no

YYYY-MM-DD

batch_id

string

no

Batch for rollback tracking

Create a fixed price contract for Acme Corp titled "Website Redesign" for $15,000, starting March 1 through June 30

list_contracts

Parameter

Type

Required

Description

client_id

int

no

Filter by client

status

string

no

DRAFT, SENT, ACTIVE, COMPLETED, EXPIRED, TERMINATED

Show me all active contracts

Lookups

list_clients

Parameter

Type

Required

Description

active

bool

no

Only active clients (default: true)

List all my clients

list_projects

Parameter

Type

Required

Description

client_id

int

no

Filter by client

active

bool

no

Only active projects (default: true)

What projects does Acme Corp have?

Batches

start_batch

Parameter

Type

Required

Description

name

string

yes

Descriptive name for the batch

Start a batch called "February 2026 expenses"

list_batches

No parameters. Shows all batches with items, counts, active flag, and rollback status.

rollback_batch

Parameter

Type

Required

Description

batch_id

string

yes

Batch ID from start_batch

Roll back the February expenses batch

Receipts / OCR

process_document

Parameter

Type

Required

Description

file_path

string

yes

Path to the document (PDF, PNG, JPG, JPEG, GIF, BMP, TIFF, WEBP)

batch_id

string

no

Batch for rollback tracking

default_tax_category

string

no

DEDUCTIBLE (default), PARTIALLY_DEDUCTIBLE, NON_DEDUCTIBLE

dry_run

bool

no

Extract only; do not create expenses

Dry run on C:\receipts\receipt.png so I can review before creating

process_documents_folder

Parameter

Type

Required

Description

folder_path

string

yes

Folder containing documents

batch_name

string

no

Batch name (defaults to folder name)

default_tax_category

string

no

DEDUCTIBLE (default)

dry_run

bool

no

Extract only; do not create expenses

Automatically starts a batch (unless dry_run).

Process all the documents in C:\statements\2026-Q1 and call the batch "Q1 2026 statements"


Batch rollback

Bulk data entry with an LLM needs an undo button. Batches provide it:

  1. start_batch("week of Feb 24") — becomes the active batch.

  2. Every subsequent create_expense / create_invoice / generate_invoice / create_contract (and OCR-created expense) records {type, id, endpoint} in the active batch, unless an explicit batch_id is given.

  3. rollback_batch(batch_id) issues DELETE on each recorded endpoint in reverse creation order, reports per-item success/failure, and marks the batch rolled back (it cannot be rolled back twice).

State is persisted to WORKPULSE_MCP_DATA_DIR/batches.json (default ~/.workpulse-mcp/) so batches survive server restarts. Rollback deletes data in WorkPulse — it is not reversible.

Example session:

Start a batch called "week of Feb 24". Then create these expenses:

  • Feb 24: $45.99 at Staples for supplies

  • Feb 25: $89.00 for Adobe Creative Cloud, software

  • Feb 26: $234.00 at Delta Airlines, travel

That last one was personal — roll back the "week of Feb 24" batch


OCR document processing

Optional. The process_document* tools send files to a DriftMoney OCR pipeline (POST /parse, DocTR + Ollama) and map its output to WorkPulse expenses:

PDF / image → DocTR OCR → Ollama LLM → DriftMoney DSL → WorkPulse expenses

Set OCR_PIPELINE_URL (default http://localhost:8000). Income and credit transactions are filtered out; debt information from card/loan statements is returned as informational data but not inserted.

DriftMoney category

WorkPulse category

Food, Groceries, Dining, Shopping, Retail, Office

SUPPLIES

Software, Subscription, SaaS, Digital

SOFTWARE

Travel, Transportation, Gas, Fuel, Uber, Lyft, Airline, Hotel

TRAVEL

Equipment, Hardware, Electronics

EQUIPMENT

Contractor, Subcontractor, Freelance

SUBCONTRACTOR

Utilities, Insurance, Medical, Entertainment, and anything else

OTHER

Use dry_run=true first and review before creating; folder processing always wraps creates in a batch.


Environment reference

Variable

Default

Purpose

WORKPULSE_API_URL

http://localhost:8080

WorkPulse REST base URL

WORKPULSE_USERNAME

Required. WorkPulse login

WORKPULSE_PASSWORD

Required. WorkPulse password

WORKPULSE_MCP_DATA_DIR

~/.workpulse-mcp

Batch state directory

OCR_PIPELINE_URL

http://localhost:8000

DriftMoney OCR pipeline

MCP_TRANSPORT

stdio

stdio · streamable-http · sse

MCP_HOST / MCP_PORT

127.0.0.1 / 8888

Bind address for HTTP transports

MCP_PATH

/mcp

Streamable HTTP endpoint path

MCP_STATELESS

false

Stateless Streamable HTTP mode

MCP_AUTH_MODE

auto

static · jwt · introspection · none

MCP_BEARER_TOKEN

Pre-shared token (static mode)

MCP_RESOURCE_URL

http://<host>:<port>/mcp

Public URL of this endpoint; OAuth resource identifier

MCP_OAUTH_ISSUER

Authorization server issuer URL

MCP_OAUTH_AUDIENCE

MCP_RESOURCE_URL

Expected aud claim

MCP_OAUTH_JWKS_URL

discovered

Explicit JWKS URL

MCP_OAUTH_ALGORITHMS

RS*/ES*/PS256

Allowed JWT algorithms

MCP_OAUTH_INTROSPECTION_URL

RFC 7662 endpoint

MCP_OAUTH_CLIENT_ID / _SECRET

Credentials for the introspection call

MCP_OAUTH_REQUIRED_SCOPES

Scopes every token must carry

See .env.example for an annotated template.


Development

pip install -e ".[dev]"
pytest                                  # 45 tests: AuthManager, verifiers, SDK middleware, end-to-end
./run-inspector.sh                      # MCP Inspector against stdio (reads .env)
mcp dev src/workpulse_mcp/server.py     # alternative Inspector launcher

The test suite needs no WorkPulse instance: REST calls are mocked with respx, and the end-to-end test drives the real server through the SDK's Streamable HTTP app in-process.

Layout:

src/workpulse_mcp/
  server.py        MCPServer construction, transport selection, lifespan
  http_auth.py     TokenVerifier implementations + build_auth_from_env()
  auth.py          WorkPulse JWT session (login / refresh / re-login)
  client.py        httpx wrapper with bearer header, retries, error mapping
  batch.py         BatchTracker (persistent, rollback in reverse order)
  ocr_pipeline.py  OCR client + DriftMoney → WorkPulse mapping
  tools/           one module per tool group
tests/             pytest (+ pytest-asyncio, respx)
examples/          client config templates

Requires mcp>=2.0 (the 2.x SDK renamed FastMCP to MCPServer and moved host/port to run()).


License

MIT — see LICENSE. Copyright (c) 2026 Andrew Carr.

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    MCP server for EZ@Work — manage clients, projects, time entries, and invoices from Claude through natural conversation. OAuth 2.1 with Google sign-in, multi-currency support.
    5
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables turning any personal-assistant REST backend into Claude-ready tools via a single MCP server, providing 38 tools for communications, finance, health, and more.
    MIT
  • F
    license
    B
    quality
    C
    maintenance
    A local, zero-dependency MCP server to author, lint, deploy, and debug Workato recipes from Claude. It wraps the Workato Developer REST API, RLCM package API, and Data Tables record API with 56 tools.
    56
    -
  • F
    license
    B
    quality
    C
    maintenance
    A standalone MCP server that enables Claude Desktop to manage Clio legal practice matters, documents, billing, and more via ~46 tools, with secure OAuth and audit logging.
    46
    -