使用说明
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/healthhttp://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)
- host:
- Env file: point to your
.envif 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
Dockerfilehandles. - If you want, the next best step is for me to add an Unraid-friendly prebuilt image workflow or a more explicit
/docsexample set for n8n payloads.
Useful endpoints:
GET /healthGET /capabilitiesPOST /workflows/process-email-previewPOST /workflows/process-emailPOST /workflows/process-inboxPOST /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=CancelledandSync 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:
- Gmail Trigger or Gmail node gets an email
- Set/Transform node maps it into the JSON shape above
- 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.
- Copy
.env.exampleto.env - Fill in your Notion credentials, Google credential file paths, and database IDs
- Start the app normally
Example:
cp .env.example .env
uvicorn app.main:app --reload
Important notes:
PPMCP_NOTION_API_KEYis 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
.envpaths 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, andTime Required - The project schema is set up for
Status,Area,Parent Project,Target Deadline,Importance,Priority(checkbox),Budget, andScoresorting - If your Projects database has both a
Notesrelation 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.Contextsis a relation), you must set:PPMCP_CONTEXTS_DB__DATABASE_ID=<your_contexts_db_id>PPMCP_CONTEXTS_DB__TITLE_PROPERTY=NamePPMCP_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_containschecks 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.edudoes not matchhsc.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 hoursProject: Residency Rotation Trackingin 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}]}