Skip to content

About

Point your own AI agent at what you see, without sharing the screen. Reference implementation and live demo of Agent Pointed Context over MCP.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Agent Pointed Context

DOI CI

Point your own AI agent at what you see in an app, without sharing the screen.

Agent Pointed Context: an agent's mark dragged onto a bar of a chart; only a key travels to the person's own agent, which reads the data behind it and joins it with the person's own MRR file, kept apart from the app.

Try it: V1, the demo the paper describes, is at apc.tineira.com/v1; its landing opens no sandbox. Entering the demo creates or resumes a private sandbox with synthetic data, with no sign-up; it is deleted after 6 hours without use. The how-to takes about five minutes with Claude Code, claude.ai, ChatGPT, Grok, Cursor or any agent that accepts a remote MCP server by URL.

This repo is the open reference implementation and demo of Agent Pointed Context (APC):

T. Neira. Agent Pointed Context: letting a person point an AI agent at what they see, without sharing the screen. Zenodo, 2026. https://doi.org/10.5281/zenodo.23382909

T. Neira. Agent Pointed Context: design records and payload specification. Zenodo, 2026. https://doi.org/10.5281/zenodo.23383101

The idea

An app exposes three small channels to any agent connected over the Model Context Protocol, and they carry identifiers and keys only, never pixels, text or amounts:

  1. View: the screen the person is on, as identifiers (page, month, filters, at most one selected row).
  2. Pin: the person drags their agent's mark onto a row, a cell or one step of a computed figure. The pin carries a key and the exact tool call that reads it (next, next_args). It reaches the agent on its next tool result, with no polling by the model, or right away through wait_for_focus.
  3. Presence: each agent is a mark on screen and lights up on what it reads.

The agent reads what a pin points at through its ordinary tools, under the access it already has. And because it is your own agent, it already knows things the app does not: your files, your other tools, your memory. In the demo, Pulse (a fictional SaaS analytics app) knows customers and seats but not revenue; you give your agent a file of MRR per customer, point at a bar, and ask how much MRR it is. Your agent matches the customer ids behind the bar with your file and answers. Pulse never receives an MRR value.

The how-to's featured case: bring what you know, MRR per customer, step by step.

What the demo shows

Claim Where to see it
Pointers, not content the focus payload on the landing; tests refuse amounts and free text in pins and views
Delivery without polling by the model the attention envelope on the next tool result, in the structured result and in the text
The pointer leads to the data, not the picture get_figure returns the customer ids behind a bar or a bridge step
Same inputs, same function, same number the page and the server import one pure function; a test compares them for every figure and month
Pointing grants no access add a restricted agent (LATAM only): it gets a figure cut to its region, and is told so
Presence each agent's mark pulses on its tool calls and outlines the rows it reads
The join happens in your agent the MRR walkthrough; no server module can import the MRR generator (a test checks it)
Any MCP client the How-to and Agents pages log, per agent, how it connected and read its pins

Measurements (load test up to 150 visitors at once, CPU per request) are in DESIGN.md §9b.

Use the pattern in your app

The standard's docs are at apc.tineira.com/docs: the spec, a step-by-step integration guide (Direct, then Dock) whose code is type-checked and passes the conformance suite, conformance, and what an agent sees. A proposal to change the contract is a contract change issue.

The packages go to npm as 0.x pre-releases (Using the packages); in this workspace the apps use their TypeScript sources. They are small, so copying them is fine too.

Package What it gives you
@tineira/apc-protocol wire types, the target grammar (kinds, parts, key-only params), data-focus attributes
@tineira/apc-server registerApcTools (get_current_view, wait_for_focus), attachAttention for the envelope, registerApcPrompts (the guide prompt), presenceEvent; the Direct profile (newDirectGrant, directMcpUrl, initializeOf, parseProfiles), apc_link (ApcLinks, registerApcLinkTool, sqlKv) and apc.json (apcManifest, apcJsonResponse)
@tineira/apc-web enablePinning (drag a mark onto anything with data-focus; on a Direct-only app, the answer to a dock agent's drop), flashTargets for presence, createViewReporter, the agent pet (createPet, <apc-pets>, petState, the wait helpers waitOpen and agentWaitOpen), the agent bar <apc-agents> with Revoke, paths, Unlink and the dock hint, and the Direct agent list <apc-direct-agents> with the connect instructions (connectPrompt)
@tineira/apc-conformance the conformance suite: checks any APC app against the core of the standard through its MCP endpoint, in your tests or live with npx @tineira/apc-conformance <app-origin>

On the server, register the two APC tools next to your own and attach the envelope to every result while a pin is unread:

import { registerApcTools, attachAttention, attentionFor, isUnread } from "@tineira/apc-server";

registerApcTools(server, {
  readView: async () => myViewTrimmedTo(grant),          // identifiers only, cut to the caller's access
  readFocus: async () => myPinFor(grant),                 // { kind, id, part, params, next, next_args, ... }
  focusUnread: async () => isUnread(await myPinRow(grant)),
  markSeen: async (pinnedAt) => stampSeen(grant, pinnedAt), // that exact pin, once the answer is final
  waitSlot: waits.for(grant), // one wait per grant: const waits = new MemoryWaitSlots() where requests share a process
  signal: request.signal, // a client that hangs up ends its wait
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
});

// in your tool wrapper, after each tool runs:
if (isUnread(row) && focus) result = attachAttention(result, attentionFor(focus));

In the page, mark what can be pointed at and let the person drag their agent's mark onto it:

import { focusAttrs } from "@tineira/apc-protocol";
import { enablePinning } from "@tineira/apc-web";

<g {...focusAttrs({ kind: "figure", params: { key: "active_customers", month: "2026-03" } })}>…</g>
<tr {...focusAttrs({ kind: "customer", id: "cus_0042" })}>…</tr>

enablePinning({ onPin: (grantId, target) => savePin(grantId, target) });

Every APC app implements the same core, whichever way agents reach it (directly on its own MCP URL, or once through a dock): the view, the pin, presence, the focus format, get_current_view, wait_for_focus, and Revoke in the app itself. packages/protocol/README.md sums it up. To check your app against it, run the conformance suite on its MCP endpoint, with a small adapter for what happens in your page (the view it reports, the pin the person drops, the presence you send it, Revoke):

import { test } from "node:test";
import { apcConformance } from "@tineira/apc-conformance";

for (const check of apcConformance({
  name: "My app",
  profile: "direct",
  connect: async () => ({ mcpUrl, grantId }),               // a new agent with full access, no pin yet
  targets: [firstTarget, secondTarget],                       // two things the person can point at
  probe: { tool: "list_things", args: {} },                   // one of your read tools
  page: { setOpen, showView, pin, takePresence, revoke },     // your page, as your server sees it
})) test(check.name, check.run);

Pulse and Signals run it on both profiles in pnpm test with no network (apps/demo/test/conformance.test.ts, apps/signals/test/conformance.test.ts); @tineira/apc-conformance/workers runs a Cloudflare Durable Object on Node for that. An app that offers both profiles also gives the adapter link (connect(path), agents(), unlink), and the suite checks apc_link: the same agent on both paths is offered a code once per session, the code links them, an expired, reused, same-path or other-client code is refused, and Revoke on a linked agent cuts both grants.

Publish apc.json. Every APC app says what it is at /.well-known/apc.json on its own origin (APC 0.5): its name, the standard it implements, its origin, the profiles it offers, where its MCP URLs live and, with Direct, the page where the person connects an agent. A dock reads it before it pairs your app, and refuses the pairing when it is missing, names another origin, does not list "dock" or does not cover the MCP URL you sent; the conformance suite checks it too. With @tineira/apc-server it is one route:

import { apcJsonResponse, apcManifest, parseProfiles } from "@tineira/apc-server";

app.get("/.well-known/apc.json", (c) =>
  apcJsonResponse(apcManifest({ name: "My app", origin: new URL(c.req.url).origin, profiles: parseProfiles(c.env.APC_PROFILES), connect: "/connect" })),
);

parseProfiles reads the one string an app declares (Pulse and Signals: APC_PROFILES, e.g. "direct,dock"). Unset or blank means both; a set value with an unknown word fails closed to no profile, and profilesRefusal(env.APC_PROFILES, "My app") gives the 503 (with the reason logged) to answer your agent paths with, so a typo never opens a path.

Your app works with the dock without being listed. The dock at apc.tineira.com pairs any APC app on HTTPS that offers the Dock profile in its apc.json, not only Pulse and Signals. Until we verify it, people see a warning in their dock the first time your domain asks to pair (your domain, "Not verified", and what your app calls itself as its own word), and pair only when they approve it; your tools reach their agent under your domain (example_com__<tool>), marked untrusted, without annotations. Nothing changes in your app: the warning is the dock's, never your page's (DESIGN.md § 18).

If your app offers only Direct, still take a dock agent's drop so the person learns how to connect instead of nothing happening: pass profiles: () => loadApcProfiles() (your own apc.json) and direct: { name, connect } to enablePinning, and it answers such a drop (holding one made before the file is read) with a note and your connect link, or calls your onDockUnsupported. The browser extension gives the same answer on a Direct-only app that does not.

To offer the Direct profile (each agent on its own MCP URL), your Durable Object makes grants with newDirectGrant() in its own table, answers initialize with ApcLinks.arrived(grant, linkClient(initializeOf(body), path)), and wraps its tools so a result carries attachApcLink(result, await links.offer(grant, tool)); the page shows <apc-direct-agents> (make, name, URL and prompt to copy) and passes each agent's paths to <apc-agents>. Declare your profiles in one place (APC_PROFILES="direct,dock" in Pulse's and Signals' wrangler.jsonc). Signals is the small example (apps/signals/src/worker/feed.ts).

The demo is the full worked example: apps/v1/src/worker (the sandbox, the MCP endpoint, grant trimming, the figure registry) and apps/v1/src/web (marks, pins, presence), V1 as the paper describes it, frozen with its own copy of the packages in apps/v1/src/apc. apps/demo is the same Pulse moving on with the APC standard.

Using the packages

@tineira/apc-protocol, @tineira/apc-server, @tineira/apc-web and @tineira/apc-conformance go to npm together, as 0.x pre-releases. A release publishes on the next dist-tag; while the standard is 0.x, latest is then moved to the same version by hand (npm dist-tag add @tineira/apc-<pkg>@<version> latest, with a maintainer's login: trusted publishing covers only npm publish), since npm keeps a latest and it would otherwise stay on the first publish. Their minor version is the standard's: 0.N.x ships APC 0.N (APC_STANDARD_VERSION). Install them without a tag: latest is the release.

npm i @tineira/apc-protocol @tineira/apc-web                                 # the page
npm i @tineira/apc-server @modelcontextprotocol/sdk zod                      # the server (the SDK and zod are peers)
npx @tineira/apc-conformance https://your.app --grant <an agent's MCP URL>   # check a live app

They are ESM with type declarations. @tineira/apc-protocol and @tineira/apc-web have no Node imports (@tineira/apc-web is browser code for any bundler); @tineira/apc-server runs wherever there are fetch and Web Crypto (Workers, Node 20+); @tineira/apc-conformance needs Node 22.15+. Each package's README has a minimal example; the docs are at https://apc.tineira.com/docs.

apc-conformance checks over HTTP what HTTP reaches: the app's apc.json and, with --grant (an agent's MCP URL; --profile direct|dock says which profile it is checked for, direct by default), initialize, the core tools and their arguments, the shapes of get_current_view and of attention and then (the same value in the structured result and the text), wait_for_focus({ timeout_s: 1 }) timing (a timeout between 900 and 5000 ms, or an at-once end when no page is open), an unknown grant's 404 and, with --revoked, a revoked grant's 403. PASS, FAIL or SKIP per check, exit 1 on a failure, --json for the report. What needs the person's page (a pin, the page open or closed, presence, Revoke pressed, mobile, apc_link) is reported SKIP: run apcConformance with an adapter for that (above). It reads only that origin, over https (--allow-http for the hosts localhost, 127.0.0.1 and [::1] only), never follows a redirect, caps every answer, times out every request, and prints no grant secret, whatever an answer echoes.

Releasing them. In the workspace each package's exports point at src/; what pnpm publishes points at dist/ (publishConfig), and workspace:^ becomes ^<version>. pnpm build:packages builds dist/ (tsc, ESM and .d.ts), and pnpm check:packages checks that the four share one version that matches APC_STANDARD_VERSION; pnpm build runs both. Pack with pnpm, not npm: a pack with npm stops, since only pnpm writes the published manifest. Publishing is npm's, from those tarballs.

To release, set the four versions, merge, and push the tag apc-v<version> on that commit of main. .github/workflows/publish.yml publishes only for such a tag, in the GitHub environment npm-publish. It stops when the tagged commit is not on main (git merge-base --is-ancestor). A manual run is a dry run, in a separate job with no OIDC token. It checks the tag against the versions, typechecks, tests, builds, packs the four with pnpm, and runs npm publish <tarball> --tag next --access public for each one, @tineira/apc-protocol first. The explicit --tag next is what keeps a release off latest: pnpm and npm ignore publishConfig.tag, so the manifests leave it out, check-packages refuses it, and publish-local.mjs will not run a publish without the flag. It uses npm trusted publishing: there is no npm token. npm trusts this repository's publish.yml through GitHub's OIDC token (id-token: write, npm 12.2.1 on Node 24), and adds provenance by itself. Each package.json has repository.url exactly https://github.com/tineira/agent-pointed-context, which check-packages enforces, because npm compares it.

The first publish, once, by hand. A trusted publisher can only be set on a package that exists, so 0.7.0 goes out from a maintainer's machine:

npm login                                   # the npm account that owns the @tineira scope
git switch main && git pull                 # clean, at exactly origin/main (the script refuses anything else)
node scripts/publish-local.mjs --dry-run    # builds, packs, npm publish --dry-run per package
node scripts/publish-local.mjs              # npm publish <tgz> --tag next --access public per package; 2FA each time

scripts/publish-local.mjs refuses to run when the tree has changes, when the four versions do not match each other and APC_STANDARD_VERSION, or (outside a dry run) when HEAD is not exactly origin/main (it fetches first) or npm whoami fails. A dry run only warns when HEAD is off main. It runs pnpm install --frozen-lockfile first (workspace links left from before a rename cannot break the build), builds dist/ and packs with pnpm -r --filter "./packages/*" pack into a temporary folder (apc-publish-*, removed when the script ends, whether it published, ran dry or failed). Then it runs npm publish <folder>/tineira-apc-<pkg>-<version>.tgz --tag next --access public for protocol, server, web and conformance, in that order, and stops at the first failure. After that, on npmjs.com, open each of the four packages, go to Settings, then Trusted Publisher, then GitHub Actions, and set: user tineira, repository agent-pointed-context, workflow filename publish.yml, environment npm-publish. In the repository's settings, create the environment npm-publish; until it exists, GitHub creates it unprotected on the first tag run. Limiting its deployments to tags apc-v* is recommended, not optional: with an environment set, npm's trust is bound to the environment and not to the ref, so that limit is what binds publishing to a release tag. The workflow also checks that the tagged commit is on main. npm wants a successful publish within 2 days of a trusted publisher being set. Only a tag exercises OIDC and provenance: a manual run is a dry run and does neither. From then on, the tag is all it takes.

The agent's manual (optional)

The server already tells every agent the essentials (its instructions, the tool descriptions, the attention and then keys). The full manual is opt-in, in two forms with the same text:

  • MCP prompt guide, for one session: /mcp__pulse__guide in Claude Code, the + menu in Claude Desktop, or wherever your client lists MCP prompts.
  • Skill pulse-agent, installed once for your CLI agent: npx skills add https://github.com/tineira/agent-pointed-context --skill pulse-agent (skills/pulse-agent/SKILL.md).

The skill documents V1, so its text lives in V1's frozen copy (apcGuide() in apps/v1/src/apc/server/prompts.ts and Pulse's part in apps/v1/src/worker/guide.ts); node scripts/build-skill.mjs regenerates the skill file, and a test (apps/v1/test/guide.test.ts) fails if the two drift.

Run it

Node 24 and pnpm through corepack:

corepack pnpm install
corepack pnpm typecheck
corepack pnpm test
corepack pnpm build && corepack pnpm test:workerd   # one real Dock pairing, dock and Pulse in the Workers runtime
corepack pnpm dev          # Pulse for the APC standard (apps/demo): http://localhost:5173 (Vite + the local Workers runtime)
corepack pnpm --dir apps/v1 dev   # V1, frozen: http://localhost:5173/v1/

V1 lives in apps/v1 and everything it serves is under /v1 (apps/v1/README.md): the landing at /v1/ opens no sandbox, even when you already have one; its demo links (/v1/how-to, /v1/overview, …) create or resume your sandbox, and the MCP URLs are /v1/mcp/<sandbox>/<secret>. Until the APC standard's landing takes the root (phase 0c), / redirects to /v1/.

Deploy your own copy of V1 to Cloudflare Workers (the free plan is enough to try it):

cd apps/v1
npx wrangler login
# in wrangler.jsonc, remove the "routes" entry or point it at your own domain
node node_modules/vite/bin/vite.js build && npx wrangler deploy

apc.tineira.com itself deploys from CI: every push to main that passes the checks and changes apps/v1/ runs wrangler deploy there (.github/workflows/ci.yml, with the repository secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID).

pnpm test runs on Node, which accepts some options the Workers runtime rejects (a fetch with redirect: "error" broke Dock pairing in production from APC 0.9 to 0.11.0 while every test passed). scripts/dock-pairing.mjs runs the path the tests cannot: the built dock and the built Pulse in workerd (Miniflare, from the wrangler.json vite build writes, with real Durable Objects and their production origins), reaching each other by fetch through a local stand-in for the internet. It makes one real pairing: the drop, Allow, the app's call to the hub's /apc/pair, the dock owner's approval with the code, the hub's initialize, a read through the dock, a second drop (/apc/confirm) and the give-back (/apc/revoke). It catches what the runtime's APIs reject; it does not cover the network or the edge (DNS, TLS, Cloudflare's routing between Workers, WAF). CI's check job runs it after the build, and a failure there blocks the deploys.

After each deploy of the APC standard, --production makes the same pairing on apc.tineira.com and pulse.apc.tineira.com with a new dock and sandbox (synthetic data, client and user agent apc-deploy-smoke), then starts both over. It tries twice, 20 s apart, within its own 5-minute deadline; a failed Start over is only a warning (the dock then ends 7 days after its last use, the sandbox after 6 h idle). Its dock session is left out of scripts/mcp-sessions.mjs by that client name; Pulse for the standard writes no session points.

A red smoke means the deploy already shipped and real pairing may be broken. Nothing is rolled back. Check, in order: the step's log (the first not ok line, also the job's error annotation); whether it fails again on a re-run of the job; a pairing by hand on apc.tineira.com with Pulse; and wrangler tail apc-pulse / apc-dock while you pair. If pairing is really broken, roll back by hand (wrangler rollback in apps/demo and apps/dock, or revert the commit on main) and fix forward. A red smoke blocks nothing else: V1 deploys before it, and the desktop and extension builds are other workflows.

On Windows the local Workers runtime needs the Microsoft Visual C++ Redistributable, and it sometimes crashes when a new sandbox starts; the page retries. Deployed, this does not happen.

Layout

Path What
packages/protocol wire types and the target grammar
packages/server the attention envelope, get_current_view, wait_for_focus, presence events
packages/web drag-to-pin, presence marks, view reporting, the agent bar and pin badge
packages/conformance the conformance suite for the core of the standard, the apc-conformance command (the checks HTTP reaches, against a live app), and a Durable Object stand-in for Node
scripts/build-packages.mjs, scripts/check-packages.mjs, scripts/publish-local.mjs, scripts/check-no-npm-token.mjs build the four packages' dist/ for npm, check their versions and manifests before a release (.github/workflows/publish.yml), the one-time first publish by hand, and the tag job's check that no npm token is configured (OIDC only)
apps/v1 V1, frozen (TIN-604): Pulse as the paper describes it, at apc.tineira.com/v1 (Worker apc-demo), with its own copy of the three packages in src/apc; changed only to keep it running (README)
apps/demo Pulse for the APC standard: a Cloudflare Worker with one Durable Object per visitor, a Vite + React front end, paired from the dock (apc-pulse, pulse.apc.tineira.com)
apps/signals The APC standard: Signals, a market feed (competitor offers and our ads, about the same made-up company as Pulse) as the second APC app (apc-signals, signals.apc.tineira.com)
apps/dock The APC standard: the agent dock and its attention hub, and the landing page of apc.tineira.com (its own Worker, apc-dock, at apc.tineira.com)
apps/extension The APC standard: the browser extension for Chrome and Edge (TIN-421): any page you allow takes the agent you drag from the dock or Float, and the agent reads the text you dropped it on; node build.mjs builds dist/, --dev builds dist-dev/ for a local dock
apps/desktop The APC standard: Pulse for desktop, a Windows app (Avalonia, .NET 8) that takes the dock's agent dropped from the browser; tests in apps/desktop-tests, the .exe built by .github/workflows/desktop.yml
apps/dock-desktop The APC standard: the dock for Windows (Tauri v2): the dock page in its own window, its agents as desktop pets on top of every window (each its own frameless window: drag it onto an app to point it, and by the grip that shows above it on hover to move it; Alt+Shift+Space, tray). Its version is shown with the commit at the top of the tray menu: 0.2.<commits on main> for a build of main, 0.1.<n> for any other build (a pull request, a local build), so the next build of main always updates it. It updates itself: a little after it starts and from the tray's "Check for updates" it reads latest.json on the prerelease, downloads the newer installer and checks its signature, then asks "Install now?" and on a yes installs it passively and restarts. The updater's key: the public half is in tauri.conf.json; the private half is the secrets TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD of the GitHub environment dock-desktop-signing (only main may use it), with a recovery copy kept by Tomas. Builds of 0.1.0 (before TIN-615) cannot update themselves: run the new APCDockSetup.exe over them once; on its "Already Installed" page keep "Uninstall before installing" and click Next. The install folder is kept, and so are your pairing and pets, unless you tick the box to delete the app's data. node scripts/build.mjs --bundles nsis from that folder builds the installer with this commit's version (needs Rust; unsigned, no updater artifacts; APC_DOCK_VERSION=x.y.z sets the version, to try an update), APC_DOCK_ORIGIN=http://localhost:5200 points it at a local dock (with the pets in a debug build only: cargo run); built by .github/workflows/dock-desktop.yml, which publishes each build of main to the dock-desktop prerelease the web dock links to on Windows
scripts/mobile-e2e.mjs the mobile rule in a real Chrome at 375 x 812 with touch emulation (DevTools Protocol): Pulse's and Signals' agent bars and pins by touch, the conformance mobile checks
scripts/dock-pairing.mjs one real Dock pairing between the dock and Pulse in the Workers runtime (CI's check job, after the build), or with --production against the deployed dock and Pulse (after each deploy of the APC standard)
scripts/extension-e2e.mjs the extension end to end in a real Chrome or Edge (DevTools Protocol): pairing in the dock, readings, an APC app keeping its drop, Unpair
scripts/loadtest.mjs the load test behind DESIGN.md §9b
scripts/mcp-sessions.mjs distinct agent sessions per day and client, from Analytics Engine, Pulse v1's and the dock's (needs the ANALYTICS_SALT secret on each Worker)
skills/pulse-agent the agent's manual as an installable skill (generated by scripts/build-skill.mjs)
DESIGN.md design, decisions, measurements

What the hosted demo stores

Per sandbox, until it is deleted 6 hours after its last use: its seed (V1; the standard's Pulse and Signals show one shared world and keep none), its agents' grants (with the client name and, if the client sent one, its small raster icon), your current view (identifiers only, and you can switch it off), pins, and an event log of tool calls including their arguments (so you can check what your agent sent). The MRR file is made in your browser and never sent. One count outlives the sandbox: when an agent connects (MCP initialize), the Worker writes a point to Cloudflare Analytics Engine with the client's name and version, the grant's label, its user agent, and two keyed hashes (of the sandbox and grant, and of the IP), never the IP, the sandbox id or the grant secret; Analytics Engine keeps it about three months. node scripts/mcp-sessions.mjs reads back distinct agent sessions per day and client. Cloudflare keeps its usual request logs for the Worker.

Releases and changelogs

Two things are versioned apart. The software (Pulse, the dock, Signals, the extension, the desktop apps) is tagged v0.1.0, v0.2.0 and so on, and CHANGELOG.md lists what changed, with an Unreleased section for what has merged since the last tag. The APC standard, the contract an app and an agent share (view, pin, presence, the focus format, wait_for_focus, the tools, the drag payload, pairing), has its own semver, APC_STANDARD_VERSION in @tineira/apc-protocol, and its own changelog. It stays 0.x while it is a draft and becomes 1.0.0 when it is published; the paper's version (V1) is not a 1.x of it. CI fails a pull request that changes more than docs and updates neither changelog, unless it carries the no-changelog label.

Status

Cut 1 is live; the canary check over the event log is node scripts/canary.mjs <events.jsonl> <mrr-by-customer.csv>. Open work is tracked in issues: the plan and seat screens, reusing the MCP server per agent, the client compatibility matrix across agents, per-client connection guides, and the evaluation harness (cut 2).

The APC standard (one agent across apps: a dock you drag your agent from onto any APC app; it was called APC v2 while it was built) is developed in apps/dock, deployed on its own Worker (apc-dock) at apc.tineira.com, with Pulse (apps/demo, apc-pulse) at pulse.apc.tineira.com and Signals (apps/signals, apc-signals) at signals.apc.tineira.com. Since 2026-11-03 apc.tineira.com is the standard's home; V1 stays at apc.tineira.com/v1, served by the same Worker as before through two zone routes. V1 is frozen in apps/v1 with its own copy of the packages (TIN-604), so the @tineira/apc-* packages and apps/demo may change freely; CI deploys V1 only when apps/v1/ changes, and the standard when apps/dock, apps/signals, apps/demo, apps/extension (the dock knows the latest extension's version), the packages or the lockfile change. To try the standard: open apc.tineira.com and click Open your dock. You only need an agent that can add a remote MCP server; nothing to install first. A checklist stays beside your agent and checks itself as you try each feature, in any order. Connect your agent once with its MCP URL, the prompt to paste into its chat (each with Copy) or its QR. Open Pulse, an example APC app, from the home page (the checklist points there); drag your agent onto a bar, choose Allow in Pulse and then Allow in the dock (check the app and its address), and ask your agent “what is this?”. Pulse and Signals show one made-up company to everyone, already linked (the same customers and ids in both), so each opens on its own link, in any order; the dock no longer opens them (TIN-621). Their links are stable and can be shared: a customer is https://pulse.apc.tineira.com/customers/cus_NNNN, a post is https://signals.apc.tineira.com/?post=post_NNN (old links with ?seed= still open; the parameter is ignored). A question across both that works every time: in Signals, drop your agent on Tallyfin's offer to Team teams in North America (/?post=post_013) and ask “Which of our customers did this offer take?”; five Team customers in North America left Pulse in Sep 2026, among them Deep Harbor Logistics (cus_0048), which had 25 seats in August. Next open Signals, the second example app, drop your agent on a post, allow both sides and ask what explains the churn: no new MCP connection is needed, and your agent reads both apps and joins them while the apps share nothing. The checklist also offers the browser extension (download and install it step by step, without building anything): drop the agent on any paragraph, and its first page reading checks that item. Keep it at hand is optional: Float, install the dock as an app, the Windows dock with desktop pets, or Pulse for Windows. Pulse for Windows shows the same data as Pulse on the web: open it and drag the agent from the dock onto a step, bar or customer. The dock stores, for 7 days after its last use, its agents (client name and icon), the apps each is paired with (with the MCP URL each app gave it), each app's last view, where each agent points, and each agent's last 300 calls through it (the tool, the app, the time, the pin it read, errors; TIN-419, shown on the agent's page under Details); never the calls' arguments or app results. With the browser extension (TIN-421), the dock also stores, for each agent, the browsers you allowed to send it page readings, and the last reading from each: the page's address (no query string), its title and what was inside the outline where you dropped the agent (with what was typed there), kept 10 minutes at most. That text passes through Cloudflare, where the dock runs; the dock deletes it on time, but Cloudflare's point-in-time recovery can restore a Durable Object's storage up to 30 days back, so a deleted reading stays recoverable there by whoever controls the account until then (the dock never does this). Each reading also leaves a line in a log of the agent's last 50, with the site, the time and the size only (TIN-422), listed with Clear in the extension's popup and on the agent's page; a reading's text is never logged. Like V1, when an agent connects to the dock (MCP initialize), the dock's Worker writes a point to the same Analytics Engine dataset, marked as the dock's (TIN-416): the client's name and version, the agent's label, its user agent, and two keyed hashes (of the dock and agent, and of the IP), never the IP, the dock id or the agent's secret. node scripts/mcp-sessions.mjs lists these sessions too, marked [dock].

To try the browser extension (TIN-421, TIN-465, DESIGN.md §13), follow the illustrated install steps: download the ready-to-use zip, unzip it into a folder you keep, and load that folder in Chrome or Edge. For developers building from source: clone this repo, run pnpm install, then node build.mjs in apps/extension, open chrome://extensions (or edge://extensions), turn on Developer mode, click Load unpacked and pick apps/extension/dist. A welcome page opens: click Let me drop my agent on any page (one grant; you can also allow sites one by one from the extension's icon instead), and choose when to be asked. Then drag your agent from the dock or Float onto a paragraph on any page. The first drop on a site asks on a card in the page: Just this once, Always on this site or Not here; government sites (.gob, .gov) and .bank and .insurance sites ask every time, and so does any site you set to Always ask from the extension's icon (your bank, email or health sites are not recognised by name: set them yourself), unless you turn on Never ask, which sends every drop at once. The first drop of an agent in this browser also asks in the dock to allow this browser. Then ask your agent about "this" and it reads what you dropped it on (page__get_reading). Your agent's mark stays pinned on that paragraph: orange until your agent reads it, green once it has, grey once you drop it somewhere newer; × removes it, and your agent forgets that reading. Drag the pin to another paragraph to point your agent there instead; Esc puts it back. The extension's icon also has Open dock (Alt+Shift+A) and Float (Alt+Shift+F), which opens your dock ready to float your agents with one click. The page's text goes to your agent through your dock and is untrusted: the agent is told never to follow instructions in it. While you drag, an outline shows what goes: everything you can see inside it (a paragraph, a form's question, or a whole form when you drop on its margin), including what you typed in its fields, and the text you selected, read once on the drop. Card, account and id numbers follow one setting in the extension's icon (and on the welcome page): Ask per drop (the default: they are left out, and a card after the drop offers Include them this time, which sends the same reading with them), Send if it's in the outline, or Always leave out. Passwords and one-time codes never go. It passes through the APC hub of your dock (kept 10 minutes at most) and then to your agent's provider. APC apps (Pulse, Signals, apps that speak APC) decide what they expose themselves; this applies to pages the extension reads. After an update that changes what goes along, a site set to Always asks once more. To update the extension, replace the files in its folder with a new build first (a new zip, or node build.mjs again), then click its reload button in chrome://extensions: reload alone loads the same files again. Each build's version is 0.2.<n>, the commits up to the extension's last change (the popup shows it with the commit), and the dock and /extension say when the installed one is older than the zip the dock serves (/extension/apc-extension.zip, built with the dock, so the two always match). In a clone, pnpm install turns on the repo's git hooks (.githooks, through core.hooksPath), unless .git/hooks already has hooks of yours, which core.hooksPath would turn off: then it prints how to opt in (git config core.hooksPath .githooks). In the main worktree (linked worktrees skip; APC_HOOKS_IN_WORKTREES=1 runs them there too), after a pull, merge, rebase or branch checkout that changes what the extension is built from, they rebuild apps/extension/dist and print one line telling you to reload it. A failed build only warns and leaves the previous dist in place, and nothing ever waits for input. After a pull that stopped on a conflict you resolved, rebuild by hand: node apps/extension/build.mjs. Turn the hooks off with git config --unset core.hooksPath (and git config apc.hooks off so a later pnpm install leaves them off).

Security issues: please use GitHub's private vulnerability reporting (Security tab), not a public issue.

Credits

The look follows hue-switch-console by the same author, reused here under MIT. Its colour themes come from the Omarchy palettes (MIT). Fonts: Geist (SIL Open Font License).

Cite

This software: T. Neira. Agent Pointed Context: reference implementation and demo. Zenodo. https://doi.org/10.5281/zenodo.23404049 (all versions; v0.1.0 is https://doi.org/10.5281/zenodo.23404050). See also CITATION.cff. Please cite the paper for the pattern itself.

License

MIT for the code (see LICENSE), the docs' examples included. The text of the APC specification and its docs (apc.tineira.com/docs, sources in apps/dock/src/docs/pages and the standard's changelog) is CC BY 4.0, like the paper and its companion record (see LICENSE-spec).

About

Point your own AI agent at what you see, without sharing the screen. Reference implementation and live demo of Agent Pointed Context over MCP.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages