usemycontext
v1.4.2
Published
The official UseMyContext SDK for the personal context layer for AI - a typed client + React hook for the structurally-blind personal-context MCP. npm i usemycontext, then drop the user's compiled context straight into any AI app.
Maintainers
Readme
usemycontext
MIT license | usemycontext.ai | Docs | GitHub | Claude Code plugin | What is a context layer?
Try it in 30 seconds, no signup. The demo account works two ways: sign in on the web at usemycontext.ai (the "Try the live demo" button), or connect any MCP client to
https://mcp.usemycontext.ai/mcpand use it at the OAuth sign-in. Login:[email protected], code424242(fixed - no mailbox needed). A shared, read-only account with a full profile, files, and a shared context. Docs: https://usemycontext.ai/docs
The user's context, in any AI app you ship. UseMyContext.ai is the personal
context layer for AI: one npm i, one hook, and the user's compiled personal
context drops straight into your prompts - while the UseMyContext public
surface stays structurally blind to the content (it declares no data
bindings, so it holds no readable user data, which lives on an isolated data
plane behind it).
The same context your users connect to Claude and ChatGPT, plus coding tools like Gemini CLI and Cursor, is what this SDK reads - one profile, every AI.
Connected AIs are proactive about it: the server's own MCP guidance tells
them to check the user's profile and documents before saying they do not know
something about the user - so the user should not have to name UseMyContext
for their own context to be used. The same guidance closes the loop at the
other end: when a conversation has surfaced something new and durable about
the user and is wrapping up, the AI is told to offer to save it with
suggest_update before the context is lost, as a pending suggestion the user
reviews and accepts.
npm i usemycontextimport { useMyContextReact } from "usemycontext/react";
// The user pasted their UseMyContext token (grab one at usemycontext.ai/#/connect).
// This component connects, then drops their compiled context into a prompt.
function Assistant({ token }) {
const { state, connected, connect, client } = useMyContextReact();
async function run() {
await connect({ token });
const { composite } = await client.profile(); // the user's compiled context
const { passages } = await client.ask("what should I keep in mind?");
const systemPrompt =
`You are helping a specific person. Their context:\n${composite}\n\n` +
`Relevant notes:\n${passages.map((p) => p.text).join("\n")}`;
console.log(systemPrompt); // -> hand this to your LLM
}
return (
<button onClick={run} disabled={state === "connecting"}>
{connected ? "Connected" : "Use my context"}
</button>
);
}That is the whole integration. The hook holds the user's own token and calls the UseMyContext MCP server exactly like Claude does - same trust model, new client shape.
Without React
The core entry is framework-free:
import { useMyContext } from "usemycontext";
const ctx = useMyContext();
await ctx.connect({ token }); // v1 is TOKEN-IN
const { composite } = await ctx.profile();The drop-in button
import { ConnectMyContext } from "usemycontext/react";
<ConnectMyContext token={token} onConnect={(s) => console.log(s)} />;It renders one <button class="umc-connect-button" data-umc-state="..."> whose
label tracks the connect state. Style it however you like - the markup is yours.
The CLI: npx usemycontext pull
Pull your curated context into a local session-starter file that any AI tool can read at startup:
export UMC_TOKEN=<your token from the Connect panel at usemycontext.ai>
npx usemycontext pull # writes ./UMC-CONTEXT.md
npx usemycontext pull --out notes/me.md # or anywhere you likeThe file is your compiled composite - never raw facts, files, or audit - wrapped in a fixed preamble that tells the consuming AI it is human-curated ground truth that must not be rewritten or appended to, plus a date stamp and a pointer back to your live profile. Overwrites are idempotent; any failure exits non-zero with a plain-English message and never leaves a partial file.
The token can also live in a token=<value> line in ~/.usemycontext. It is
deliberately never accepted as a CLI flag (flags land in shell history), and
the CLI never writes it anywhere. Pulls show up as their own client in your
activity feed instead of blending into your other AIs.
Wire it into Claude Code as a SessionStart hook (in .claude/settings.json):
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "npx -y usemycontext pull" }] }
]
}
}For agent frameworks that read a bootstrap file at startup, point the
bootstrap list at the pulled UMC-CONTEXT.md. More recipes:
https://usemycontext.ai/docs/cli-pull
The tools
The MCP server exposes thirteen tools, and the client gives you an ergonomic
wrapper for every one of them. Once connected, call the reads and the one write
below - all read-only against your files. One tool, suggest_update, can write:
it files a pending suggestion that you review and approve in the web app. Nothing
the AI does ever edits your profile or your files directly, and every access
leaves a record in your audit trail.
| Method | Returns | What it is |
|---|---|---|
| client.profile({ projectId?, handle? }) | { composite, facts? } | the compiled personal context - drop into a system prompt |
| client.listProfiles() | ProfileListResult | list_profiles - your own profiles (name, @handle, privacy), metadata only |
| client.info() | InfoResult | info - static about-the-service metadata, capability boundary, doc links |
| client.account() | AccountSummary | account - your current plan, limits, and storage usage (read-only; usage.storageUsedBytes is null when the figure could not be read - show it as unknown, not zero) |
| client.ask(query, { k?, projectId?, handle? }) | { passages, text } | ask_docs - cited passages to ground the LLM |
| client.queryTable(fileId, query, { projectId?, handle? }) | TableQueryResult | query_table - EXACT counts/sums/filters over one tabular file |
| client.listFiles({ projectId?, handle? }) | FileMeta[] | the user's files (metadata only) |
| client.searchFiles(query, { projectId?, handle? }) | FileMeta[] | search files by name |
| client.getFile(fileId, { projectId?, handle? }) | { text, downloadUrl?, image?, imageBase64?, mimeType? } | the file's full extracted text, or an inlined image |
| client.search(query, { projectId?, handle? }) | { results: [{ id, title, url }] } | search - the deep-research ALIAS of search_files (same data, ChatGPT's contract shape) |
| client.fetchDoc(id, { projectId?, handle? }) | { id, title, text, url, metadata? } | fetch - the deep-research ALIAS of get_file (full extracted text in ChatGPT's contract shape) |
| client.suggestUpdate(text) | { id?, text } | the one write - a PENDING suggestion the user reviews |
| client.sharedContext(selector?) | { mode, shares, composite } | read contexts shared with you, opened to the network, or a teammate's |
Files: getFile and images
getFile(fileId) returns a document's full extracted text plus a short-lived
downloadUrl:
const { text, downloadUrl } = await client.getFile("resume.pdf");When the file is an image, the service inlines it and the result carries the base64 bytes so you can render it directly (a text/document result leaves these undefined):
const file = await client.getFile("headshot.png");
if (file.image && file.imageBase64) {
const dataUrl = `data:${file.mimeType};base64,${file.imageBase64}`;
// <img src={dataUrl} /> - the picture, not a link
}An image too large to inline (over the ~4MB guard) falls back to downloadUrl
with a short note in text (no imageBase64). getFile also surfaces the
parsed name, contentType, and a truncated flag when the text was cut at the
service's size guard.
FileMeta (from listFiles / searchFiles) carries the file id, name,
size, contentType, modified, and (when known) the owner-side status, a
user-chosen displayName, and hasText (whether a processed file yielded
searchable text).
Deep-research aliases: search and fetchDoc
The server also exposes search and fetch - thin read-only ALIASES of
search_files and get_file in the result shape ChatGPT's deep research
requires. Same data, same permissions, same audit trail; only the shape differs.
client.search(query) returns { results: [{ id, title, url }] }, and
client.fetchDoc(id) returns { id, title, text, url, metadata? } with the
file's full extracted text (fetchDoc is named so a destructured method never
shadows the global fetch; on the wire it calls the MCP tool named fetch).
Prefer searchFiles / getFile in your own code - they carry richer metadata,
download links, and inline images.
Tables: queryTable
queryTable(fileId, query) runs an EXACT, deterministic query over ONE tabular
file (a CSV, the first table of a spreadsheet/PDF/Word document, or a table read
out of an image). Reach for it
instead of ask whenever the question needs counting, summing, averaging,
min/max, filtering, or exact row lookups over structured data - semantic search
undercounts big tables, while this executes over every row and returns exact
numbers. The query is a small constrained DSL (TableQuery):
// count the rows whose status is "failed"
const failed = await client.queryTable("f_orders", {
where: [{ col: "status", op: "eq", value: "failed" }],
aggregates: [{ fn: "count" }],
});
// -> { columns: ["count"], groups: [{ count: 42 }], rowCountMatched: 42, ... }
// total amount per region
await client.queryTable("f_orders", {
groupBy: "region",
aggregates: [{ fn: "sum", col: "amount" }],
});
// return the matching raw rows
await client.queryTable("f_people", {
select: ["name", "email"],
where: [{ col: "country", op: "eq", value: "FR" }],
});op is eq | neq | contains (text, case-insensitive) or gt | gte | lt | lte
(numeric); fn is count | sum | avg | min | max (col required except for
count); where clauses are AND-combined; limit defaults to 50 (max 200). The
result is TableQueryResult - rows (select mode) OR groups (aggregate mode),
plus rowCountTotal / rowCountMatched / truncated / tableSource
("csv" | "extracted" | "image"; an image table was read out of a picture, so
treat its numbers as approximate). A typed
engine refusal (an unknown column - the message lists the file's real columns - a
non-tabular file, and so on) throws a ToolCallError.
sharedContext selectors
Call sharedContext() with no argument to LIST the contexts others shared
with you (metadata only). To READ one, pass a grantId string (back-compatible) or
an options object with exactly one selector:
await client.sharedContext(); // LIST shares given to you
await client.sharedContext("g_alice"); // READ a share by grantId
await client.sharedContext({ from: "g_alice" }); // same, object form
await client.sharedContext({ handle: "@carol" }); // READ a network-open project by @handle
await client.sharedContext({ org: "o1", member: "m2" }); // READ a teammate's live context
await client.sharedContext({ saved: "carol-notes" }); // READ a context you saved by nameorg + member optionally take orgProject to pick which of the teammate's
projects to read. When the access behind a selector is gone (revoked, expired,
left the team, or an unknown saved name), the result is a clean unavailable read
(mode: "read", composite: null) - never a false success.
Structured results
The UseMyContext server declares an MCP output schema on every tool and returns
a machine-readable structuredContent object alongside the rendered text. Since
v1.1.0 the SDK reads that structured result directly whenever it is present;
when structuredContent is absent or malformed, the client falls back to
parsing the text exactly as before - older servers and text-only results keep
working with no code change on your side.
You do not need to do anything to benefit: the wrappers return the same typed
shapes as before, just sourced more reliably. Two small additions ride along:
getFile results can now carry content (the file's extracted text alone,
without the header and download-link wrapping), and sharedContext reads can
carry owner (who the served context belongs to). The structured shapes are
exported as types (StructuredProfile, StructuredFileList, StructuredGetFile,
StructuredAskDocs, StructuredSuggestUpdate, StructuredSharedContext) if you
work with raw results.
Projects (projectId or handle)
profile, ask, queryTable, listFiles, searchFiles, getFile, search,
and fetchDoc accept an optional projectId - or, equivalently, the project's own
public @handle (with or without the leading @). Both apply only to an "all"-scope
connection and are resolved against your own projects by the service; a single-project
token ignores them (the token's own project binding wins).
const { composite } = await client.profile({ projectId: "p2" });
const files = await client.listFiles({ projectId: "p2" });
// Same thing, addressed the way a person says it:
const work = await client.profile({ handle: "@my-work" });handle must name one of your own profiles. A handle belonging to anyone else - or
one nobody holds - is refused with the same error either way (so a private handle stays
unguessable), and it is never quietly swapped for a different profile of yours. When both
selectors are supplied the handle wins. To read a context somebody else shared with you
or published, use sharedContext({ handle }), which is a different read entirely.
The connect state machine
ctx.state is one of:
idle -> connecting -> connected
\-> declined (user/host refused)
\-> error (network / transport failure)
connected -> expired (token rejected mid-session - re-connect)
<any> -> disconnected (disconnect() was called)A 401 from the server (an expired or revoked token) moves the client to
expired and throws TokenExpiredError; re-run connect({ token }) with a
fresh token to recover. Revoking is enforced on the server: the token dies
immediately, everywhere - the user can revoke any connection from the web app's
Connect page, and nothing needs cleaning up on your side.
Errors
Every failure is a typed subclass of UseMyContextError:
NotConnectedError- a wrapper was called beforeconnect().TokenExpiredError- the server rejected the token (carries theWWW-Authenticateheader).ScopeDeniedError- this connection is not authorized for that tool.ToolCallError- the tool returned an error result.TransportError- the network/transport failed.
Token model (v1)
v1 is token-in only: you supply the user's UseMyContext session token to
connect({ token }). The full browser-OAuth-in-the-SDK flow (a hosted popup on
connect.usemycontext.ai) is v2.
Persistence
Default: the token is kept in memory only and is never written to
localStorage unless you opt in. It does not survive a page reload by default.
This is deliberate, but be clear about what it does and does not do. The v1 token
is the user's long-lived session credential. Memory-only persistence reduces
its exposure - the token is never written to disk, is gone on reload, and cannot
be read at rest from localStorage - but it does not eliminate it: while the
client is connected the token still lives in the host page's memory, so a script
running on your page (an XSS) could still read it during a live session.
The long-lived-token-in-the-host-page problem is not solved by storage choice.
It is solved by v2's token lifecycle: short-lived access tokens plus an
httpOnly refresh cookie on connect.usemycontext.ai, so the host page never holds
a long-lived credential at all. Until then, treat the v1 token as sensitive and
keep your page free of untrusted scripts.
If you want the token to survive reloads, opt in knowingly (this trades the
above reduction away - the token is now also readable at rest from localStorage):
import { useMyContext, webStorage } from "usemycontext";
// You accept that the token now lives in localStorage, readable by any
// script on the page. The httpOnly-cookie model (no token in the host page)
// lands in v2.
useMyContext({ storage: webStorage(localStorage) });Pass storage: null to use a no-op store, or supply your own StorageAdapter:
useMyContext({ storage: null });License
MIT
