个人生产力MCP服务

一个模块化的Python MCP服务,提供基于schema的任务、项目、笔记创建,邮件转任务,以及Google日历与Notion事件同步等个人生产力自动化工作流,适用于自托管个人效率管理场景。

  • by DrMichaelLarsen
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 DrMichaelLarsen/personal_mcp_agent

使用说明

Personal Productivity MCP

A modular Python starter for a schema-aware personal productivity system that exposes opinionated MCP tools and uses LangGraph only for multi-step workflows.

Included in v1

  • Schema-aware task, project, and note creation services
  • Conservative project matching with confidence metadata
  • Preview-first tagged email processing workflow
  • Preview-first day planning workflow
  • FastAPI health/capabilities endpoints
  • MCP tool/resource registration scaffold
  • Fake adapters and tests for core business logic
  • Email workflow starter that structures unread/unprocessed email into task-ready page content with summary, action items, and original email body

Email-to-task workflow direction

The internal workflow engine now has a first-pass email processing design intended for your Gmail inbox workflow:

  • fetch unread/unprocessed candidate emails
  • classify them
  • analyze each email through an AI-analysis service boundary
  • generate structured task page content with:
    • summary
    • action items checklist
    • original email content
  • map as many task properties as possible
  • create or preview the task result

Right now the "AI" analysis step is implemented as a replaceable service boundary with deterministic heuristics so the architecture is ready before wiring a live LLM.

Running the API

Install the runtime dependencies first:

pip install -e .[dev]

Then run the API:

uvicorn app.main:app --host 0.0.0.0 --port 8000

Hosting on Unraid

The easiest way to run this on Unraid is as a Docker container.

1. Put the project somewhere on your cache/appdata share

Example:

/mnt/user/appdata/personal-productivity-mcp/

Put these files there:

  • project source
  • .env
  • optional docker-compose.yml

2. Create your .env

Copy .env.example to .env and fill in your values.

Important Unraid-style paths:

PPMCP_GMAIL__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json
PPMCP_CALENDAR__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json

That means your Google credential JSON should exist on the host at:

/mnt/user/appdata/personal-productivity-mcp/secrets/google-oauth-client.json

because the compose file mounts:

/mnt/user/appdata/personal-productivity-mcp/secrets -> /config/secrets

3. Build and run

From the app folder on Unraid:

docker compose up -d --build

Then open:

  • http://UNRAID-IP:8000/health
  • http://UNRAID-IP:8000/capabilities

4. n8n integration

From n8n, send requests to:

http://UNRAID-IP:8000/workflows/process-email-preview

or

http://UNRAID-IP:8000/workflows/process-email

5. If you prefer the Unraid Docker UI instead of compose

Use these settings:

  • Repository/Build: build from this folder or prebuild the image
  • Port mapping: 8000 -> 8000
  • Volume mapping:
    • host: /mnt/user/appdata/personal-productivity-mcp/secrets
    • container: /config/secrets
    • mode: read-write (Google refresh tokens are updated in place)
  • Env file: point to your .env if your workflow supports it, or enter the same variables manually

Notes

  • The app is ready to run in Docker now.
  • You still need valid Python dependencies inside the container, which the Dockerfile handles.
  • If you want, the next best step is for me to add an Unraid-friendly prebuilt image workflow or a more explicit /docs example set for n8n payloads.

Useful endpoints:

  • GET /health
  • GET /capabilities
  • POST /workflows/process-email-preview
  • POST /workflows/process-email
  • POST /workflows/process-inbox
  • POST /workflows/sync-calendar

Google Calendar <-> Notion Events sync

The agent can now replace the 2sync calendar workflow. It uses the existing Notion Events schema (Event ID, Date, Item Link, Calendar Name, Description, Location, Sync Status, and related fields).

Default behavior is intentionally Google authoritative:

  • reads Google events for the next 92 days
  • creates or updates the matching Notion event page using Event ID
  • adds the Notion page URL to the Google event description
  • stores the Notion page ID/URL in Google private extended properties
  • maps cancellations to Event Status=Cancelled and Sync Status=Removed
  • handles timed and all-day events (including Google's exclusive all-day end date)

Enable the 15-minute background sync with:

PPMCP_CALENDAR__TOKEN_PATH=/config/secrets/token.pickle
PPMCP_CALENDAR_SYNC__ENABLED=true
PPMCP_CALENDAR_SYNC__INTERVAL_MINUTES=15
PPMCP_CALENDAR_SYNC__LOOKAHEAD_DAYS=92
PPMCP_CALENDAR_SYNC__MODE=google_authoritative
PPMCP_CALENDAR_SYNC__CALENDAR_IDS=["primary"]
PPMCP_CALENDAR_SYNC__EDITABLE_CALENDARS_ONLY=true
PPMCP_CALENDAR_SYNC__STATE_PATH=/data/calendar_sync_state.json

For all editable calendars, use PPMCP_CALENDAR_SYNC__CALENDAR_IDS=["*"]. With PPMCP_CALENDAR_SYNC__EDITABLE_CALENDARS_ONLY=true, calendars where your Google account is only a reader or free/busy reader are skipped. For a controlled subset, use the exact Google calendar IDs in the JSON list.

Run a safe preview before enabling automatic writes:

curl -X POST http://UNRAID-IP:8001/workflows/sync-calendar \
  -H "Content-Type: application/json" \
  -d '{"dry_run":true,"days_ahead":92}'

Then commit a Google-authoritative sync:

curl -X POST http://UNRAID-IP:8001/workflows/sync-calendar \
  -H "Content-Type: application/json" \
  -d '{"dry_run":false,"days_ahead":92,"mode":"google_authoritative"}'

Optional two_way mode uses a persistent fingerprint state file. A Notion-only edit is pushed to Google; Google wins if both sides changed since the prior run. This is an event-level decision, not a field-by-field merge or a latest-timestamp comparison: even edits to different fields on both sides resolve in Google's favor. Without saved state for an existing linked event, Google wins on the initial sync. Keep the state file persistent across restarts.

An unlinked Notion event page in the window creates a Google event in the calendar whose Google name exactly matches its Calendar Name selection (or the configured calendar_name_property). Only calendars included in the sync request/configuration are eligible. A blank selection defaults to the first included calendar. An unknown or ambiguous name produces a per-page error and does not create an event; use unique Google calendar names. Ignored and Removed pages are not created in Google. Changing Calendar Name on an already-linked event does not move it to another calendar. Google cancellations mark the Notion page cancelled/removed; deleting a Notion page does not delete the Google event. Keep google_authoritative unless you need two-way creation and edits.

The Notion integration must be shared with the Events database, and the Google OAuth token must include https://www.googleapis.com/auth/calendar. OAuth token refresh writes the token file, so /config/secrets cannot be mounted read-only.

Sending one email from n8n

Yes — now you can POST a normalized email payload from n8n directly into this app.

Example request to preview a single email:

POST /workflows/process-email-preview
{
  "email": {
    "id": "gmail-message-id-123",
    "thread_id": "gmail-thread-id-123",
    "subject": "Project Alpha: please review this",
    "sender": "person@example.com",
    "body": "Please review this proposal and send feedback by tomorrow.",
    "received_at": "2026-03-24T17:30:00Z",
    "labels": ["INBOX", "UNREAD"]
  },
  "preview_only": true,
  "confidence_threshold": 0.8
}

Example n8n flow:

  1. Gmail Trigger or Gmail node gets an email
  2. Set/Transform node maps it into the JSON shape above
  3. HTTP Request node sends it to:
    • POST http://YOUR-HOST:8000/workflows/process-email-preview
    • or POST http://YOUR-HOST:8000/workflows/process-email

Suggested n8n HTTP Request body:

{
  "email": {
    "id": "{{$json.id}}",
    "thread_id": "{{$json.threadId}}",
    "subject": "{{$json.subject}}",
    "sender": "{{$json.from}}",
    "body": "{{$json.textPlain || $json.snippet}}",
    "received_at": "{{$json.internalDate}}",
    "labels": "{{$json.labelIds}}"
  },
  "preview_only": true,
  "confidence_threshold": 0.8
}

Use preview first, then switch to commit mode once you trust the extraction.

Gmail token requirement for inbox processing

/workflows/process-inbox uses the Gmail API adapter directly and requires a refreshable OAuth token file.

Set in .env:

PPMCP_GMAIL__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json
PPMCP_GMAIL__TOKEN_PATH=/config/secrets/gmail-token.json

Both files should exist on Unraid host under:

/mnt/user/appdata/personal-productivity-mcp/secrets/

because that folder is mounted into the container as /config/secrets.

If token is missing/expired and cannot refresh, inbox processing will fail with a clear runtime error.

Quick start

python -m venv .venv
.venv\Scripts\activate
pip install -e .[dev]
uvicorn app.main:app --reload
pytest

Configuration with .env

The app loads settings from a .env file in the project root, which is a better fit for Unraid, Docker, and other self-hosted setups.

  1. Copy .env.example to .env
  2. Fill in your Notion credentials, Google credential file paths, and database IDs
  3. Start the app normally

Example:

cp .env.example .env
uvicorn app.main:app --reload

Important notes:

  • PPMCP_NOTION_API_KEY is your Notion integration secret
  • Gmail and Calendar use credential file paths, not a single API key
  • On Unraid, mount your credential JSON files into the container and point the .env paths at those mounted files
  • The task schema is now set up to map your real task fields like Contexts, Importance, Scheduled, Deadline, Assigned, Goal, dependency_of, parent, and Time Required
  • The project schema is set up for Status, Area, Parent Project, Target Deadline, Importance, Priority (checkbox), Budget, and Score sorting
  • If your Projects database has both a Notes relation and a separate text summary field, set:
    • PPMCP_PROJECTS_DB__NOTES_PROPERTY=Notes (relation)
    • PPMCP_PROJECTS_DB__DESCRIPTION_PROPERTY=Description (text/AI summary used for matching)
  • To save task contexts reliably (when Tasks.Contexts is a relation), you must set:
    • PPMCP_CONTEXTS_DB__DATABASE_ID=<your_contexts_db_id>
    • PPMCP_CONTEXTS_DB__TITLE_PROPERTY=Name
    • PPMCP_CONTEXTS_DB__STATUS_PROPERTY=Status
    • and keep PPMCP_TASKS_DB__CONTEXTS_PROPERTY=Contexts

Example Unraid/Docker-style paths:

PPMCP_GMAIL__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json
PPMCP_CALENDAR__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json

The settings model uses:

  • prefix: PPMCP_
  • nested separator: __

So PPMCP_TASKS_DB__DATABASE_ID maps to settings.tasks_db.database_id.

Project routing (sender/domain + project profile)

The email workflow now supports schema-aware project routing with sender/domain bias.

Add to .env

PPMCP_PROJECT_ROUTING__LEXICAL_WEIGHT=0.65
PPMCP_PROJECT_ROUTING__PROFILE_WEIGHT=0.25
PPMCP_PROJECT_ROUTING__SENDER_BIAS_WEIGHT=1.0
PPMCP_PROJECT_ROUTING__MAX_SENDER_BONUS=0.35

PPMCP_PROJECT_ROUTING={"domain_rules":[{"domain":"hsc.utah.edu","area_contains":["Residency"],"project_contains":["Residency"],"score_bonus":0.3},{"domain":"aah.org","area_contains":["Advocate"],"project_contains":["Advocate"],"score_bonus":0.25}],"sender_rules":[{"sender":"michael.e.larsen@hsc.utah.edu","area_contains":["Residency"],"project_contains":["Rotation"],"score_bonus":0.35}]}

Important behavior notes

  • project_contains checks against a combined project profile text that includes:
    • title
    • description/notes
    • area path
    • full project path
    • tags
  • Domain matching is exact by sender domain.
    • utah.edu does not match hsc.utah.edu
    • Add both domains if needed.
  • Exact project title present in subject/body improves deterministic matching.

Best-practice email format

Use one of these in subject/body for high-confidence routing:

  • Project Alpha: follow up on ...
  • [Project: Residency Rotation Tracking] submit duty hours
  • Project: Residency Rotation Tracking in the first lines of body

Docker/Unraid quick env snippet

PPMCP_NOTION_API_KEY=secret_xxx
PPMCP_GMAIL__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json
PPMCP_GMAIL__TOKEN_PATH=/config/secrets/gmail-token.json
PPMCP_CALENDAR__CREDENTIALS_PATH=/config/secrets/google-oauth-client.json
PPMCP_CALENDAR__CALENDAR_ID=primary

PPMCP_TASKS_DB__DATABASE_ID=...
PPMCP_PROJECTS_DB__DATABASE_ID=...
PPMCP_NOTES_DB__DATABASE_ID=...
PPMCP_AREAS_DB__DATABASE_ID=...
PPMCP_CONTEXTS_DB__DATABASE_ID=...

PPMCP_PROJECT_ROUTING__LEXICAL_WEIGHT=0.65
PPMCP_PROJECT_ROUTING__PROFILE_WEIGHT=0.25
PPMCP_PROJECT_ROUTING__SENDER_BIAS_WEIGHT=1.0
PPMCP_PROJECT_ROUTING__MAX_SENDER_BONUS=0.35
PPMCP_PROJECT_ROUTING={"domain_rules":[{"domain":"hsc.utah.edu","area_contains":["Residency"],"project_contains":["Residency"],"score_bonus":0.3}]}