dvm-contextlayer
A personal context layer for AI assistants: one Python package that bundles several Model Context Protocol (MCP) servers, each exposing a slice of your own working context. Run them together or separately, over stdio or HTTP, from any MCP client such as Claude Desktop or Claude Code.
| Server | What it exposes | Backend |
|---|---|---|
obsidian |
Full-text search, notes, tags, backlinks and daily notes | Local Obsidian vaults + SQLite FTS5 |
outlook |
Mail search, folders, messages, calendar events | Microsoft Graph (read-only) |
planner |
Plans, buckets, tasks, my tasks | Microsoft Graph (read-only) |
todo |
Microsoft To Do lists and tasks | Microsoft Graph (read-only) |
files |
OneDrive and SharePoint search, browsing and text content | Microsoft Graph (read-only) |
teams |
Chats, channels, messages and message search | Microsoft Graph (read-only) |
graph |
A personal knowledge graph: entities, relations, observations, Cypher | Local Kuzu database |
memory |
Mem0-style long-term memories with full-text recall and history | Local SQLite FTS5 |
Settings live in ~/.mycontextlayer/config.json. Secrets (the Microsoft token cache) never
touch that file: they are stored in the operating system's secure store through the
keyring library (Windows Credential Manager, macOS
Keychain, or Secret Service on Linux).
Using GitHub Copilot on Windows? Follow the step-by-step walkthrough.
Install
Python 3.11 or newer.
# recommended: an isolated tool install
uv tool install dvm-contextlayer
# or
pipx install dvm-contextlayer
# or plain pip
pip install dvm-contextlayer
This installs the mycontextlayer command.
Quick start
mycontextlayer list # what is bundled and what is configured
mycontextlayer configure obsidian # answer the prompts; settings are saved
mycontextlayer serve obsidian # run it over stdio (what MCP clients launch)
mycontextlayer client-config # JSON / commands to paste into your MCP client
A server refuses to start until it is configured and tells you which command to run:
$ mycontextlayer serve outlook
The outlook server is not configured. Run: mycontextlayer configure outlook
Connecting a client
mycontextlayer client-config prints ready-made snippets. For Claude Desktop add to
claude_desktop_config.json:
{
"mcpServers": {
"obsidian": { "command": "mycontextlayer", "args": ["serve", "obsidian"] },
"outlook": { "command": "mycontextlayer", "args": ["serve", "outlook"] }
}
}
For Claude Code:
claude mcp add obsidian -- mycontextlayer serve obsidian
claude mcp add memory -- mycontextlayer serve memory
HTTP mode (all servers in one process)
mycontextlayer configure http # optional: host and port, default 127.0.0.1:8765
mycontextlayer serve --all # every configured server, Streamable HTTP
mycontextlayer serve outlook -t http # or a single server
With --all each server is mounted under its own path, for example
http://127.0.0.1:8765/outlook/mcp; a single server is served at /mcp. GET / lists the
endpoints and GET /health is a public liveness probe. Use
mycontextlayer client-config --transport http for the matching client snippets.
HTTP authentication
HTTP mode is authenticated by default. configure http lets you pick one of three modes:
http.auth |
Who can connect |
|---|---|
api_key |
Anyone presenting the random key generated during configure http (default) |
oauth |
Anyone presenting a Microsoft Entra access token issued for your app registration |
none |
Everyone on the network (the server warns loudly) |
API key mode. configure http generates a random key, stores it in the OS keyring
(never in config.json) and prints it once. Every request except /health must carry it,
otherwise the server answers 401:
Authorization: Bearer mcl_... # or
X-API-Key: mcl_...
mycontextlayer apikey # show the key again
mycontextlayer apikey --rotate # replace it (old key stops working immediately)
claude mcp add --transport http outlook http://127.0.0.1:8765/outlook/mcp \
--header "Authorization: Bearer $(mycontextlayer apikey)"
serve refuses to start in HTTP mode when auth is on but no key is stored. In containers
without a keyring, pass the key through the MYCONTEXTLAYER_HTTP_API_KEY environment
variable instead.
OAuth mode (offered once a Microsoft app registration is configured). The HTTP host becomes an OAuth 2.0 protected resource for that same app registration: clients obtain an access token from Microsoft Entra for your API scope and send it as a bearer token. Tokens are validated locally (RS256 signature against the tenant's published keys, expiry, audience, issuer, tenant and scope), so no round trip to Microsoft is needed per request.
One-time setup on the app registration in the Entra admin center:
- Expose an API → set the Application ID URI to
api://<client id>. - Add a scope, e.g.
access_as_user(admins and users can consent). - Optionally add the calling client ids under Authorized client applications so users are not prompted for consent.
- Optionally set
requestedAccessTokenVersionto2in the manifest. Both v1.0 and v2.0 token formats are accepted.
Then run mycontextlayer configure http, choose oauth and enter the scope name. The host
serves RFC 9728 metadata at /.well-known/oauth-protected-resource (and per endpoint, e.g.
/.well-known/oauth-protected-resource/outlook/mcp) naming
https://login.microsoftonline.com/<tenant>/v2.0 as the authorization server, and answers
401 with a WWW-Authenticate: Bearer resource_metadata=... challenge, which is how MCP
clients discover where to authenticate. Entra does not support dynamic client registration,
so the MCP client must be able to use a pre-registered client id (or run its own OAuth flow
for the scope api://<client id>/<scope>). Tokens missing the scope get 403.
config.json options under http.oauth: required_scopes (default ["access_as_user"]),
audiences (extra accepted aud values; the client id and api://<client id> are always
accepted) and allowed_tenants (defaults to the configured tenant, or any tenant when the
tenant is common/organizations). For a quick test:
mycontextlayer oauth-token # device-code sign-in for the API scope, prints a token
curl -H "Authorization: Bearer $(mycontextlayer oauth-token)" http://127.0.0.1:8765/
Setting "auth": "none" in the http section (or choosing none during configure http)
disables authentication; the server then warns loudly, so only do that on a trusted,
local-only interface. Whatever the mode, put a TLS-terminating reverse proxy in front of the
server before exposing it beyond localhost, since credentials travel in a header.
Microsoft 365 setup
The outlook, planner, todo, files and teams servers share one sign-in. You sign in
once with a device code; the refresh token is kept in the OS keyring and renewed
automatically.
1. Create an app registration (once)
In the Microsoft Entra admin center go to App registrations → New registration:
- Name: anything, e.g.
mycontextlayer - Supported account types: Accounts in this organizational directory only (or multi-tenant if you want to use the same app in several tenants)
- Redirect URI: leave empty
- After creating: Authentication → Advanced settings → Allow public client flows: Yes
- Add the Microsoft Graph delegated permissions below (see Adding API permissions for the exact clicks). Add only the rows for the servers you intend to use; sign-in requests just the scopes of the servers you have configured, so an unconsented permission for an unused server never blocks you.
| Permission | Used by | Admin consent | Notes |
|---|---|---|---|
User.Read |
all | no | Always required |
Mail.Read |
outlook | no | Your own mailbox |
Calendars.Read |
outlook | no | Your own calendars |
MailboxSettings.Read |
outlook | no | Time zone and working hours; get_mailbox_settings only |
Tasks.Read |
planner, todo | no | Your Planner tasks, plans you can open, your To Do lists |
Group.Read.All |
planner | yes | Optional. Enumerates plans of every group you belong to |
Files.Read.All |
files | no | Your OneDrive plus files shared with you and site libraries |
Sites.Read.All |
files | no | SharePoint site search and site drives |
Chat.Read |
teams | no | Your 1:1 and group chats |
Team.ReadBasic.All |
teams | no | Names of teams you joined |
Channel.ReadBasic.All |
teams | no | Channel names |
ChannelMessage.Read.All |
teams | yes | Reading channel posts and replies |
Minimum set to read your own data without an administrator: User.Read, Mail.Read,
Calendars.Read, MailboxSettings.Read, Tasks.Read, Files.Read.All, Sites.Read.All,
Chat.Read, Team.ReadBasic.All, Channel.ReadBasic.All. Without Group.Read.All the
planner server still lists plans shared with you and plans referenced by your tasks.
Without ChannelMessage.Read.All the teams server can list teams, channels and chats but
channel messages return a permission error. If you only need OneDrive and no SharePoint,
Files.Read is enough in place of Files.Read.All and Sites.Read.All (set it via
microsoft.extra_scopes and remove the files defaults from the app registration).
Some tenants require admin consent for every permission; in that case ask an admin to grant
consent once for the app.
Only read permissions are requested; the servers never send mail, create tasks or modify anything in Microsoft 365. Note the Application (client) ID and Directory (tenant) ID.
Adding API permissions
In the portal (works on any browser, no tooling needed):
- Open entra.microsoft.com → Identity → Applications →
App registrations → All applications and click your app (e.g.
mycontextlayer). - In the left menu under Manage click API permissions. You will see
User.Readalready listed; it is added to every new registration. - Click + Add a permission. A panel opens on the right.
- Under Microsoft APIs click the big Microsoft Graph tile.
- Click Delegated permissions (not Application permissions: delegated means "act as the signed-in user", which is what the device-code sign-in does).
- Type a permission name in the Select permissions search box, e.g.
Mail.Read. The list is grouped by resource; expand the group (Mail) and tick the checkbox next toMail.Read. Repeat the search for each permission you need from the table above; the ticks are kept while you search. - Click Add permissions at the bottom. The panel closes and the new rows appear in the Configured permissions table.
- Look at the Admin consent required column. Rows saying No are consented by you when
you first sign in. Rows saying Yes (
Group.Read.All,ChannelMessage.Read.All) need an administrator: either click Grant admin consent for above the table if you are one, or send the app name and the permission names to your admin and ask them to press that button. Until then the Status column shows Not granted. - Nothing else to save; the permissions take effect immediately. Removing a permission is the … menu at the end of its row → Remove permission.
Your sign-in can only include permissions that are listed here; if mycontextlayer login
fails with AADSTS65001 or "needs admin approval", the permission named in the message is
either missing from this page or still waiting for admin consent.
From a script instead (Azure CLI, signed in with az login), the permission ids are looked
up from the Microsoft Graph service principal so nothing is hard-coded:
APP_ID="<your Application (client) ID>"
GRAPH="00000003-0000-0000-c000-000000000000" # Microsoft Graph
for perm in User.Read Mail.Read Calendars.Read MailboxSettings.Read Tasks.Read \
Files.Read.All Sites.Read.All Chat.Read Team.ReadBasic.All Channel.ReadBasic.All; do
id=$(az ad sp show --id "$GRAPH" --query "oauth2PermissionScopes[?value=='$perm'].id" -o tsv)
az ad app permission add --id "$APP_ID" --api "$GRAPH" --api-permissions "$id=Scope"
done
az ad app permission admin-consent --id "$APP_ID" # only if you are an administrator
2. Configure and sign in
mycontextlayer configure outlook # asks for client ID and tenant ID, then offers to sign in
# or explicitly
mycontextlayer configure microsoft
mycontextlayer login
mycontextlayer whoami
mycontextlayer logout # removes the cached sign-in from the keyring
login prints a URL and a code; open the URL, enter the code and sign in. The scopes of
every configured Microsoft server are requested in one go so you consent once; configuring
another Microsoft server later checks whether the existing sign-in covers it and offers to
sign in again if not. Extra scopes can be added to microsoft.extra_scopes in config.json.
Configuration file
~/.mycontextlayer/config.json (override the folder with the MYCONTEXTLAYER_HOME
environment variable). Example:
{
"version": 1,
"microsoft": {
"client_id": "00000000-0000-0000-0000-000000000000",
"tenant_id": "common",
"account": "you@example.com",
"extra_scopes": []
},
"servers": {
"obsidian": { "vaults": [{ "name": "Work", "path": "/Users/you/Obsidian/Work" }] },
"outlook": { "timezone": "Europe/Amsterdam", "default_page_size": 25 },
"planner": { "include_group_plans": true },
"todo": {},
"files": { "max_content_bytes": 200000 },
"teams": {},
"graph": { "database_path": "" },
"memory": { "database_path": "", "default_user_id": "default" }
},
"http": {
"host": "127.0.0.1",
"port": 8765,
"auth": "api_key",
"oauth": { "required_scopes": ["access_as_user"], "audiences": [], "allowed_tenants": [] }
}
}
A server is "configured" when its section exists. Empty database paths default to
~/.mycontextlayer/data/. The Obsidian index lives in ~/.mycontextlayer/index/. The file
contains no secrets and is written with mode 0600.
Secrets
The secrets are the MSAL token cache and the HTTP API key. Both are stored under the
service name dvm-contextlayer in the OS keyring, split into chunks small enough for Windows Credential
Manager. If no keyring backend is available (typical on a headless Linux box without a
Secret Service), configure and login fail with an explanation instead of falling back to
plain files. On Linux desktops install GNOME Keyring or KWallet; on servers, run the
Microsoft servers on a machine with a keyring instead.
The servers in more detail
obsidian indexes .md and .txt files (ignoring .obsidian, .trash, hidden folders)
into SQLite FTS5 with YAML frontmatter, inline #tags and [[wikilinks]]. Tools:
list_vaults, search_notes (FTS5 syntax), read_note, list_notes, recent_notes,
get_backlinks, list_tags, get_daily_note, reindex. Notes are also exposed as
obsidian://<vault>/<path> resources. The index refreshes incrementally when it is older
than a minute.
graph stores Entity(name, type, observations[]) nodes and RELATED(relation) edges in
an embedded Kuzu database. Tools mirror the reference MCP memory server (create_entities,
create_relations, add_observations, search_nodes, open_nodes, read_graph, delete
variants) plus get_neighbors, graph_stats and a read-only query_graph for Cypher.
memory keeps one short fact per row, scoped by user_id / agent_id / run_id, with
BM25 full-text search, duplicate detection and an audit history (add_memory,
add_memories, search_memories, list_memories, get_memory, update_memory,
delete_memory, delete_all_memories, memory_history, memory_stats). Extraction of
facts is left to the MCP client, which keeps everything local and model-free.
The Microsoft servers return compact summaries (recipients, dates, previews) and convert HTML bodies to plain text. See each tool's description in your client for parameters.
Development
git clone https://github.com/divyavanmahajan/mycontextlayer
cd mycontextlayer
uv sync --group dev
uv run pytest
uv run ruff check src tests && uv run ruff format --check src tests
uv run mycontextlayer list
Point MYCONTEXTLAYER_HOME at a scratch folder while developing so your real config is left
alone. The Microsoft servers are tested against a mocked Graph API; there are no live tests.
Releasing
Versions come from git tags via hatch-vcs; there is no version number in the source.
git tag v0.1.0
git push origin v0.1.0
The Publish to PyPI workflow runs the tests, builds the sdist and wheel, checks that the
built version equals the tag, publishes to PyPI through
Trusted Publishing and creates a GitHub release
with the artifacts. One-time setup on PyPI: under the dvm-contextlayer project (or as a
pending publisher before the first release) add a GitHub publisher with owner
divyavanmahajan, repository mycontextlayer, workflow publish.yml, environment pypi.
Also create the pypi environment in the GitHub repository settings (optionally with required
reviewers). No PyPI token is needed.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dvm_contextlayer-1.0.0.tar.gz.
File metadata
- Download URL: dvm_contextlayer-1.0.0.tar.gz
- Upload date:
- Size: 60.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34287ce82188931f49e452be84910711348dbefc3310523f1b372e046811def7
|
|
| MD5 |
74d8f88554e0fcfcd9e6aa0dd5e3389a
|
|
| BLAKE2b-256 |
9509425fae8d2b47970844137bf3988bfe499a6e1916a7adc8e38d53dd623c20
|
Provenance
The following attestation bundles were made for dvm_contextlayer-1.0.0.tar.gz:
Publisher:
publish.yml on divyavanmahajan/mycontextlayer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dvm_contextlayer-1.0.0.tar.gz -
Subject digest:
34287ce82188931f49e452be84910711348dbefc3310523f1b372e046811def7 - Sigstore transparency entry: 2762488363
- Sigstore integration time:
-
Permalink:
divyavanmahajan/mycontextlayer@23b7bda779ec3dd200b3842a9bc8ef5fb7fcca4e -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/divyavanmahajan
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@23b7bda779ec3dd200b3842a9bc8ef5fb7fcca4e -
Trigger Event:
push
-
Statement type:
File details
Details for the file dvm_contextlayer-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dvm_contextlayer-1.0.0-py3-none-any.whl
- Upload date:
- Size: 66.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e0678451762ecfdb180eb73723bda537bf10ee341591efe975a9fafd56f7937
|
|
| MD5 |
a3cffccc7a3cbb7f9800d86a42a3b0fc
|
|
| BLAKE2b-256 |
a5990e56dcb4073404556a9029659460c93201cc7e8de3c2e2b00d4d5c6db3c1
|
Provenance
The following attestation bundles were made for dvm_contextlayer-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on divyavanmahajan/mycontextlayer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dvm_contextlayer-1.0.0-py3-none-any.whl -
Subject digest:
6e0678451762ecfdb180eb73723bda537bf10ee341591efe975a9fafd56f7937 - Sigstore transparency entry: 2762488384
- Sigstore integration time:
-
Permalink:
divyavanmahajan/mycontextlayer@23b7bda779ec3dd200b3842a9bc8ef5fb7fcca4e -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/divyavanmahajan
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@23b7bda779ec3dd200b3842a9bc8ef5fb7fcca4e -
Trigger Event:
push
-
Statement type: