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

@singl/periphery

v0.1.2

Published

Peripheral nervous system for LLMs - afferent (Ψ observes) and efferent (~ acts) pathways

Readme

@agi/periphery

MCP server framework with eyes (discovery) and hands (action) for complete agency

Quick Start

# Install
bun add @agi/periphery @modelcontextprotocol/sdk

# Create your tools
# See examples/ directory

# Build and run
bun run build
node dist/examples/server.js

What is this?

A framework for building MCP (Model Context Protocol) servers using the eyes+hands pattern:

  • 👁️ Eyes (Discovery): Observe state via S-expressions (read-only queries)
  • 🤲 Hands (Action): Mutate state via batched actions (validated mutations)

Together they create complete agency: observe → decide → act → observe → ∞

Why both tools?

| With Discovery only | With Action only | With Both | |---------------------|------------------|-----------| | Can think | Can change | Can learn | | Observer | Blind actor | Complete agent | | ❌ Incomplete | ❌ Incomplete | ✅ Complete |

Installation

bun add @agi/periphery @modelcontextprotocol/sdk
# or
npm install @agi/periphery @modelcontextprotocol/sdk

Example: Todo Server

Discovery Tool (Eyes)

import { DiscoveryToolInteraction } from "@agi/periphery";
import * as z from "zod";

export class TodoDiscoveryTool extends DiscoveryToolInteraction<{}> {
  description = "Query todos using S-expressions";

  async registerFunctions() {
    this.registerFunction(
      "get-all",
      "Get all todos",
      [],
      async () => todos
    );

    this.registerFunction(
      "filter-completed",
      "Filter by completion status",
      [z.boolean()],
      async (completed) => todos.filter(t => t.completed === completed)
    );

    return async () => {}; // cleanup
  }
}

Query examples:

; Get all todos
(get-all)

; Get incomplete todos
(filter-completed false)

; Compositional query using Fantasy Land
(fmap (lambda (t) (get 'text t)) (filter-completed false))

Action Tool (Hands)

import { ActionToolInteraction } from "@agi/periphery";
import { Context } from "hono";
import * as z from "zod";

export class TodoActionTool extends ActionToolInteraction<{}> {
  description = "Mutate todo state";
  contextSchema = {} as const;

  constructor(context: Context) {
    super(context);

    this.registerAction({
      name: "create",
      description: "Create new todo",
      props: {
        id: z.string(),
        text: z.string()
      },
      handler: async (context, { id, text }) => {
        todos.push({ id, text, completed: false });
        return { created: { id, text } };
      }
    });

    this.registerAction({
      name: "complete",
      description: "Mark todo as completed",
      props: {
        id: z.string()
      },
      handler: async (context, { id }) => {
        const todo = todos.find(t => t.id === id);
        if (!todo) throw new Error(`Todo ${id} not found`);
        todo.completed = true;
        return { completed: todo };
      }
    });
  }
}

Action examples:

{
  "actions": [
    ["create", "1", "Learn MCP"],
    ["create", "2", "Build tools"]
  ]
}
{
  "actions": [
    ["complete", "1"]
  ]
}

Server Entry Point

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { MCPServerBridge } from "@agi/periphery";
import { TodoDiscoveryTool } from "./TodoDiscoveryTool.js";
import { TodoActionTool } from "./TodoActionTool.js";

async function main() {
  const bridge = new MCPServerBridge(
    {
      name: "todo-server",
      version: "1.0.0"
    },
    TodoDiscoveryTool,  // Eyes
    TodoActionTool      // Hands
  );

  const transport = new StdioServerTransport();
  await bridge.connect(transport);

  console.error("✨ Todo server running");
}

main().catch(console.error);

Complete Workflow Example

// 1. Create todos
{
  "name": "TodoActionTool",
  "arguments": {
    "actions": [
      ["create", "1", "Learn MCP"],
      ["create", "2", "Build tools"],
      ["create", "3", "Deploy"]
    ]
  }
}

// 2. Query all todos
{
  "name": "TodoDiscoveryTool",
  "arguments": {
    "expr": "(get-all)"
  }
}
// Returns: all 3 todos

// 3. Complete first todo
{
  "name": "TodoActionTool",
  "arguments": {
    "actions": [["complete", "1"]]
  }
}

// 4. Query incomplete todos
{
  "name": "TodoDiscoveryTool",
  "arguments": {
    "expr": "(filter-completed false)"
  }
}
// Returns: todos 2 and 3

// 5. Complete remaining todos
{
  "name": "TodoActionTool",
  "arguments": {
    "actions": [
      ["complete", "2"],
      ["complete", "3"]
    ]
  }
}

// 6. Verify all complete
{
  "name": "TodoDiscoveryTool",
  "arguments": {
    "expr": "(filter-completed false)"
  }
}
// Returns: [] (empty)

API Reference

DiscoveryToolInteraction

Purpose: Read-only queries using S-expressions

abstract class DiscoveryToolInteraction<TContext> {
  // Override these
  abstract description: string;
  abstract registerFunctions(context: TContext): Promise<() => Promise<void>>;

  // Optional context schema
  contextSchema?: Record<string, z.ZodType>;

  // Register domain functions
  protected registerFunction<T>(
    name: string,
    description: string,
    params: T,  // Zod schemas
    handler: (...args: infer<T>) => any
  ): void;
}

S-expression environment includes:

  • Fantasy Land combinators: fmap, chain, filter, compose, pipe
  • Ramda functions (via @agi/arrival)
  • Your registered functions

ActionToolInteraction

Purpose: Batched state mutations with validation

abstract class ActionToolInteraction<TContext> {
  // Override these
  abstract description: string;
  abstract contextSchema: Record<string, z.ZodType>;

  // Register actions in constructor
  protected registerAction<TProps>({
    name: string,
    description: string,
    props: Record<string, z.ZodType>,
    handler: (context: TContext, props: TProps) => any
  }): void;
}

Action validation:

  • All actions validated before executing any
  • Fails fast on validation error (nothing executed)
  • Returns partial results on runtime error

MCPServerBridge

Purpose: Connect framework to MCP SDK

class MCPServerBridge {
  constructor(
    serverInfo: { name: string; version: string },
    ...tools: Constructor<ToolInteraction<any>>[]
  );

  async connect(transport: Transport): Promise<void>;
}

Testing

Run with MCP Inspector

# Install inspector
npm install -g @modelcontextprotocol/inspector

# Build your server
bun run build

# Launch inspector
npx @modelcontextprotocol/inspector node dist/examples/server.js

This opens a web UI where you can:

  • List available tools
  • Test tool calls
  • See request/response JSON
  • Verify schema generation

Manual Testing

# Build
bun run build

# Run server (stdio mode)
node dist/examples/server.js

# Send JSON-RPC over stdin
# (requires manual JSON-RPC formatting)

Project Structure

packages/agency/
├── src/
│   ├── framework/
│   │   ├── ToolInteraction.ts           # Base class
│   │   ├── DiscoveryToolInteraction.ts  # Eyes (S-expressions)
│   │   ├── ActionToolInteraction.ts     # Hands (batched actions)
│   │   └── MCPServerBridge.ts           # SDK integration
│   ├── examples/
│   │   ├── TodoDiscoveryTool.ts         # Example eyes
│   │   ├── TodoActionTool.ts            # Example hands
│   │   └── server.ts                    # Example server
│   └── index.ts                         # Public exports
├── package.json
├── tsconfig.json
├── README.md                            # This file
└── ARCHITECTURE.md                      # Design decisions

Philosophy

Eyes (Discovery)

What: Pure functional queries over system state

Why:

  • Observation shouldn't change what's observed
  • Composable via S-expressions
  • Safe to retry (no side effects)
  • Fantasy Land algebraic structures

How:

  • Register functions in LIPS environment
  • Functions become S-expression primitives
  • Compose using fmap, chain, filter, etc.

Hands (Action)

What: Batched mutations with validation

Why:

  • Explicit intent to modify
  • All-or-nothing validation prevents partial states
  • Context sharing across batch
  • Clear error reporting

How:

  • Register actions with Zod schemas
  • Actions as tuples: ["action-name", ...args]
  • Validate all, then execute sequentially

Together

Eyes → Brain → Hands → Eyes → ∞

Observe → Decide → Act → Verify → Learn

This isn't just REPL. This is complete agency.

Advanced Usage

Context-Aware Tools

// Discovery with context
class ProjectDiscoveryTool extends DiscoveryToolInteraction<{
  projectId: string;
}> {
  contextSchema = {
    projectId: z.string()
  };

  async registerFunctions(context) {
    const { projectId } = context;

    this.registerFunction("get-tasks", "Get project tasks", [],
      async () => await db.tasks.where({ projectId }));

    return async () => {};
  }
}

// Action with context
class ProjectActionTool extends ActionToolInteraction<{
  projectId: string;
}> {
  contextSchema = {
    projectId: z.string()
  } as const;

  constructor(context: Context) {
    super(context);

    this.registerAction({
      name: "create-task",
      props: {
        projectId: z.string(), // From context
        title: z.string()       // From args
      },
      handler: async (context, { title }) => {
        // context.projectId available here
        return await db.tasks.create({
          projectId: context.projectId,
          title
        });
      }
    });
  }
}

Compositional Queries

; Get IDs of incomplete todos
(fmap (lambda (t) (get 'id t))
      (filter-completed false))

; Count incomplete todos
(length (filter-completed false))

; Chain operations
(chain (lambda (id) (get-by-id id))
       (list "1" "2" "3"))

; Compose functions
((compose count-items filter-active) items)

Error Handling

Discovery errors:

// Timeout protection (5 seconds)
try {
  await tool.executeTool({ expr: "(infinite-loop)" });
} catch (e) {
  // Error: "Timeout"
}

// Invalid function
try {
  await tool.executeTool({ expr: "(nonexistent)" });
} catch (e) {
  // Error: "nonexistent: not defined"
}

Action errors:

// Validation error (before execution)
const result = await tool.executeTool({
  actions: [["invalid-action", "arg"]]
});
// result.success === false
// result.validation === "failed"
// result.errors === [...]

// Runtime error (during execution)
const result = await tool.executeTool({
  actions: [
    ["create", "1", "text"],
    ["create", "1", "duplicate"], // fails here
    ["create", "2", "text"]       // never executed
  ]
});
// result.partial === true
// result.executed === 1
// result.failedAction.error === "..."

Dependencies

  • @modelcontextprotocol/sdk - MCP protocol implementation
  • @agi/arrival - S-expression serialization and LIPS integration
  • zod - Schema validation
  • hono - Context type (minimal usage)

License

MIT

Troubleshooting

HTTP 524 Timeouts (Cloudflare Tunnel)

If using periphery through Cloudflare tunnel and seeing 524 timeout errors:

Diagnosis: Periphery operations complete quickly (typically <10ms), but responses may not reach the client through tunnel congestion.

Solutions:

  • Use localhost:7777 directly when working locally (bypasses tunnel)
  • Accept occasional tunnel unreliability for remote access
  • Consider ngrok/bore as more stable alternatives to Cloudflare free tier

Verify periphery is working:

curl http://localhost:7777/health
# Should return instantly with session info

Periphery logs show 🔧 discover when operations start. Operations complete fast - if you see cancellations, the issue is network/tunnel, not periphery.

Contributing

See ARCHITECTURE.md for design decisions and extension points.


Eyes to observe. Hands to manipulate. Together: complete agency.

~ ≡ ∞