npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lensmcp/core

v1.7.0

Published

LensMCP core — event bus, resource store, graph store, and pattern detectors.

Readme

@lensmcp/core

The framework-agnostic core of LensMCP — the event bus, reducers, and stores that turn raw agent-observability events into a live graph and live resources.

LensMCP is an observability lens for coding agents. @lensmcp/core is the engine underneath it: a tiny in-process event bus, a reducer registry that runs on top of the bus, and the two stores it feeds — a multi-axis graph store and a diff-aware resource store. It takes the normalised events defined in @lensmcp/protocol-types and turns them into the traversable graph, flow "stories", and pattern matches that the MCP server and dashboard expose.

Everything here is pure and isomorphic — no Node-only APIs, no framework dependencies (the lone runtime dependency is @lensmcp/protocol-types). It runs the same in a NestJS process, a Vite/React app, or a worker. Wiring (which reducers feed which stores) is left to the host; this package supplies the primitives.

Install

yarn add @lensmcp/core

@lensmcp/protocol-types is a peer concept — it ships as a direct dependency and provides the BaseEvent, TraceNode, TraceEdge, and EdgeAxis types used throughout.

Usage

Event bus + reducers + a resource

import {
  EventBus,
  ReducerRegistry,
  ResourceStore,
  GraphStore,
  buildGraphFromEvents,
} from '@lensmcp/core';
import type { BaseEvent } from '@lensmcp/protocol-types';

const bus = new EventBus({ capacity: 10_000 });
const graph = new GraphStore();
const resources = new ResourceStore();
const reducers = new ReducerRegistry();

// Expose the live graph as a readable resource.
const events: BaseEvent[] = [];
resources.register('graph://summary', () => ({
  nodes: graph.nodeCount(),
  edges: graph.edgeCount(),
}));

// A reducer ingests events into the graph and marks the resource dirty.
reducers.register('graph', async (event) => {
  events.push(event);
  buildGraphFromEvents(graph, events);
  await resources.markDirty('graph://summary');
});

// Feed every published event through the reducers.
bus.subscribe((event) => reducers.dispatch(event));

// React to resource changes (e.g. push an MCP notification).
resources.subscribe('graph://summary', (uri, revision) => {
  console.log(`${uri} changed → revision ${revision}`);
});

bus.publish(someBaseEvent);
const summary = await resources.read('graph://summary');

Traverse the graph along a lens

import { GraphStore } from '@lensmcp/core';

const graph = new GraphStore();
// ...nodes/edges populated by buildGraphFromEvents...

const { nodes, edges } = graph.traverse({
  from: rootNodeId,
  lens: ['render', 'state'], // one EdgeAxis or several
  direction: 'both',         // 'out' | 'in' | 'both'
  depth: 4,
  maxNodes: 1000,
});

Compile a flow story and detect patterns

import { compileStory, detectNPlusOne, detectRenderStorm } from '@lensmcp/core';

const story = compileStory(events, { flowId });
const issues = [
  ...detectNPlusOne(events, { threshold: 3 }),
  ...detectRenderStorm(events, { threshold: 5, windowMs: 250 }),
];

API

Event bus — event-bus.ts

| Export | Description | | --- | --- | | EventBus | In-process, single-threaded, bounded event bus. publish(event), subscribe(handler, filter?) → Unsubscribe, droppedCount(), subscriberCount(), capacity. Handler errors (sync and async) are swallowed so one subscriber can't break the bus. | | EventBusOptions | { capacity?: number; onDrop?: (droppedCount: number) => void } — bounded queue size (default 10_000) and an overflow hook. | | EventFilter | (event: BaseEvent) => boolean — optional per-subscription predicate. | | EventHandler | (event: BaseEvent) => void | Promise<void> — subscriber callback. |

Reducer registry — reducer-registry.ts

| Export | Description | | --- | --- | | ReducerRegistry | Ordered set of named reducers. register(name, reducer) (throws on duplicate name), list() → readonly ReducerEntry[], dispatch(event) runs every reducer against one event (errors swallowed). | | Reducer | (event: BaseEvent) => void | Promise<void> — consumes an event and may mutate stores / mark resources dirty. | | ReducerEntry | { name: string; reducer: Reducer }. |

Graph store — graph-store.ts

| Export | Description | | --- | --- | | GraphStore | Typed multi-axis trace graph (in-memory, hot-tier). addNode, updateNode, getNode, addEdge, getEdge; indexed lookups childrenOf, nodesInFlow, nodesByLogical, nodesByInstance, nodesByRequest; traverse(opts); nodeCount(), edgeCount(). | | traverse (method) | Breadth-first lens traversal returning { nodes, edges }, bounded by depth (default 4) and maxNodes (default 1000). | | Lens | EdgeAxis | readonly EdgeAxis[] — one or more axes to walk. | | TraversalDirection | 'out' | 'in' | 'both'. | | TraverseOptions | { from; lens; depth?; direction?; maxNodes? }. | | TraverseResult | { nodes: TraceNode[]; edges: TraceEdge[] }. |

Graph from events — graph-from-events.ts

| Export | Description | | --- | --- | | buildGraphFromEvents(store, events) | Bridge from the flat event log to the multi-axis GraphStore. Promotes render / state-update / server-request / user-action / loop events to nodes, then wires edges two ways: explicit causedByNodeId edges on each node's natural axis, plus a same-flow chronological spine on the caused axis. Returns GraphBuildResult. | | GraphBuildResult | { nodeCount: number; edgeCount: number }. |

Resource store — resource-store.ts

| Export | Description | | --- | --- | | ResourceStore | Registry of URI-addressed, lazily-produced resources with diff-aware revisioning. register(uri, produce) → Unsubscribe, read(uri), list(), markDirty(uri) (recomputes JSON, bumps the revision only if it changed, then notifies), revision(uri), subscribe(uri, handler) → Unsubscribe. | | ResourceValue | unknown — anything JSON.stringify can serialise. | | ResourceProducer | () => ResourceValue | Promise<ResourceValue>. | | ResourceUpdatedHandler | (uri: string, revision: number) => void. |

Story builder — story.ts

| Export | Description | | --- | --- | | compileStory(events, options?) | Compile one flow's events into a FlowStory — an ordered set of phases (user-action → loading-ui → backend-request → state-update → final-render) plus origin and final-state summary. Pass { flowId } or let it pick the first flow it sees. | | groupByFlow(events) | Map<flowId, BaseEvent[]>; events without a flowId are dropped. | | FlowStory | The compiled record: flowId, origin, phases, finalState?, startedAt, endedAt, eventCount. | | StoryPhase / StoryPhaseKind | A single phase and its kind union (user-action, loading-ui, idle-ui, route-transition, state-update, backend-request, render, final-render, visual-violation). | | CompileStoryOptions | { flowId? }. |

Pattern matching — patterns.ts

| Export | Description | | --- | --- | | detectDbInLoop(events, options?) | Flags loop events whose dbCallsInsideLoop exceeds the threshold (default 5). | | detectNPlusOne(events, options?) | Flags identical DB query signatures repeated ≥ threshold times (default 3). | | detectRenderStorm(events, options?) | Sliding-window detector: flags a component instance that rendered more than threshold times (default 5) within windowMs (default 250). | | findSlowPath(roots, adjacency, durationOf) | Memoised, cycle-guarded DFS for the slowest cumulative chain; returns SlowPathResult. | | PatternMatch / PatternKind | A detection result (kind, severity, title, evidence, details, detectedAt) and its kind union (db-in-loop, n+1-query, slow-path, render-storm, leak). | | DbInLoopOptions / NPlusOneOptions / RenderStormOptions / SlowPathResult | Per-detector option and result shapes. |

Fingerprinting — fingerprint.ts

| Export | Description | | --- | --- | | fingerprint(input) | Stable, deterministic routing key for an event (kind:<FNV-1a-32 hex>). Built from kind, identity, optional file and detail — same root cause yields the same fingerprint even when line numbers drift. Not a security hash. | | FingerprintInput | { kind; identity; file?; detail? }. |

Thresholds — thresholds.ts

| Export | Description | | --- | --- | | LensmcpThresholds | Tunable detector thresholds: dbInLoop, nPlusOne, renderStorm, slowRouteMs, slowRenderMs. | | DEFAULT_THRESHOLDS | The defaults (5, 3, 5, 500, 16). | | resolveThresholds(overrides?) | Merges a partial override over the defaults, ignoring non-finite / negative / non-numeric values so a malformed config can't disable detection. |

Ids — ulid.ts

| Export | Description | | --- | --- | | ulid(now?) | Dependency-free, time-sortable 26-char ULID-like id (48-bit time prefix + 80 bits of crypto.getRandomValues randomness, Crockford-Base32 alphabet). |

Common — common.ts

| Export | Description | | --- | --- | | Unsubscribe | () => void — the detach lambda returned by every subscribe-style API. |

How it fits

@lensmcp/core sits between the protocol and the consumers:

  • Consumes @lensmcp/protocol-types — the BaseEvent stream and the TraceNode / TraceEdge / EdgeAxis graph vocabulary are defined there; this package never invents its own event shapes.
  • Powers the LensMCP MCP server — the server registers reducers on the ReducerRegistry, ingests live events through the EventBus, and surfaces the GraphStore, stories, and pattern matches as MCP tools and ResourceStore resources (with diff-aware update notifications).
  • Powers the dashboard — the same stores back the live graph, flow stories, and detected-issue views the dashboard renders.

Because the core is framework-agnostic and isomorphic, the host process owns all wiring: it decides which reducers run, which resources are registered, and how published events flow into dispatch.


Part of LensMCP. Apache-2.0.