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

@flowbrew/plugin-sdk

v0.5.0

Published

SDK for authoring Taskflow plugins, bricks, and triggers.

Readme

@flowbrew/plugin-sdk

@flowbrew/plugin-sdk provides the schemas and helpers used to author Taskflow plugins, executable bricks, and trigger sources. It can create a validated plugin manifest, turn Zod schemas into JSON Schema, verify Taskflow invocation and trigger-lifecycle tokens, and deliver normalized trigger events to Taskflow.

Install

npm install @flowbrew/plugin-sdk

Quickstart

This minimal plugin publishes one brick that triples a number. Deploy the fetch handler at the same URL used by the brick's endpointUrl.

Handler configuration is discovered automatically from GET {origin}/.well-known/flowbrew-plugin-configuration. The optional origin defaults to https://mcp.flowbrew.app; pass an explicit origin such as https://mcp.staging.flowbrew.app for staging or a self-hosted deployment. The issuer, JWKS, and redemption URLs may use a different host and are read from the discovery response.

import {
  createBrickHandler,
  createPluginManifest,
} from "@flowbrew/plugin-sdk";
import * as z from "zod/v4";

const input = z.object({ number: z.number() }).strict();
const output = z.object({ tripled: z.number() }).strict();

export const manifest = createPluginManifest({
  slug: "number-tools",
  name: "Number tools",
  version: "1.0.0",
  visibility: "public",
  connectionTypes: [],
  bricks: [
    {
      slug: "triple-number",
      name: "Triple number",
      description: "Return the supplied number multiplied by three.",
      endpointUrl: "https://plugins.example.com/number-tools/triple-number",
      input,
      output,
    },
  ],
});

const tripleNumber = createBrickHandler({
  brickId: "number-tools.triple-number",
  input,
  output,
  handler: ({ number }) => ({ tripled: number * 3 }),
});

export default {
  fetch(request: Request) {
    return tripleNumber.fetch(request);
  },
};

createBrickHandler accepts POST requests with a Taskflow bearer token, discovers and briefly caches the environment configuration, validates the token and its brick:invoke scope, parses the request body with the input schema, redeems every slot through the discovered redemption URL, and validates the handler result against the output schema. The handler receives { claims, connections, signal } as its second argument; connection credentials remain separate from parsed workflow input, and long-running I/O should use signal so the SDK can cancel it before the caller's effective step timeout.

Handler Error.message values are caller-visible plain text at HTTP 422; empty messages and non-Error throws fall back to Brick handler failed. Write messages for workflow users and keep private diagnostics in cause, which is not serialized. The SDK redacts exact values from redeemed runtime connections. It marks these responses with x-taskflow-error-source: handler; Taskflow forwards messages up to 4096 UTF-8 bytes in the existing service error envelope, while keeping remote error bodies out of logs. Unmarked responses and messages beyond that limit retain the generic service error.

Binary-safe brick inputs and outputs use plain pointer strings. There is no shared wire schema: each brick declares its own accepted schemes with z.string().regex(...). For fetch pointers use z.string().regex(FETCH_POINTER_PATTERN) with the exported pattern; it requires fetch://https:// and survives JSON Schema export/import. Do not use refinements or brands for allowlists, or apply .url() to a pointer: a pointer is not itself a URL. The resolver also validates the embedded URL and its HTTPS protocol.

parsePointer and makePointer use scheme[+hint][;key=value]*://rest. They split at the first ://, metadata before hint, and never decode or normalize rest. Scheme, hint and key names are lowercase [a-z][a-z0-9.-]*. Metadata values are nonempty printable ASCII excluding ;, + and :. Duplicate keys are rejected. Metadata remains available to plugin-owned schemes; core base64 pointers allow an optional hint but no metadata, and fetch pointers allow neither hints nor metadata.

Core resolves exactly base64 (canonical base64, at most 10,000 body characters / 7,500 bytes) and fetch. The canonical form is fetch://https://host/path, with the full HTTPS URL in rest. HTTPS is the only supported embedded protocol. Bare URLs and the shortened fetch://host/path form are rejected. Self-issued /files/:token URLs retain their seven-day lifetime and possession grants access after unwrapping. Handler code can use resolveContent or the fetch-only resolveFetchPointer for a known-length, counted stream; length comes from actual bytes, stored objects or validated HTTP responses. Other schemes can be parsed for private plugin use but core never resolves them.

Use inlinePointer(bytes, hint?) for raw bytes, jsonPointer(value) or toContent(value) for JSON, and decodeInline(pointer) for handler-side decoding. The JSON helpers safely encode UTF-8 and return base64+json://...; the json hint supplies application/json when resolved. Use brick-specific sibling fields or HTTP headers for other content types, including parameterized MIME types. Core http-request returns only status, headers, and body; read headers["content-type"] and headers["content-length"] when present. Chunked responses can lack an advertised length, including when stored in R2. File upload still accepts an optional contentType; upload and download outputs contain only fetch pointers (or null for a missing download).

import { toContent, fetchPointer, urlOf } from "@flowbrew/plugin-sdk";

const body = toContent({ message: "Dobrý den" });
const file = fetchPointer("https://files.example.com/report.pdf");
const message = `Report: ${urlOf(file)}`;

Workflow code should pass compatible pointers unchanged and declare pointer fields as z.string() in workflow contracts. The generated ./taskflow-bricks module exports the same toContent, fetchPointer, and urlOf helpers. Never interpolate a raw pointer into human-facing text: use urlOf for fetch pointers (it throws for base64 and other schemes). Never auto-wrap a plain URL silently; reject it where a pointer is expected and require explicit fetchPointer(url). Workflow code cannot make outbound network calls; extracting a URL does not resolve its bytes (issue #172 is separate).

Migrating from the brief 1.0.0/2.0.0 npm releases (both since unpublished — see CHANGELOG): replace bare HTTPS pointer inputs with explicit fetchPointer(url), unwrap fetch pointers before using them as URLs, remove base64 ;size= metadata, and update schemas and code that read HTTP output contentType/size siblings. Replace fetchHttpsPointer calls with resolveFetchPointer. This is a hard cut, with SDK 0.5.0 and core manifest 2.1.0; pinned core versions share the mutable handler endpoint and do not preserve the previous behavior.

Set redeemConnections: false for a handler that only needs the signed connection metadata in claims.connections. In that mode no credential redemption requests are made and connections is empty.

Upload a manifest

Use the exported uploadPluginManifest({ endpoint, apiKey, manifest }) helper to validate a manifest locally and upload it as authenticated JSON. Set endpoint to the discovery response's pluginUploadUrl, which is the API-key-authenticated POST {mcp-origin}/plugins/upload endpoint. It includes the SDK version header automatically, applies a 60-second timeout to the request and response body, and rejects redirects.

The helper requires an HTTP 200 JSON response without an error field. It checks that plugin.id is a non-empty string and that plugin.slug, plugin.version, and plugin.visibility exactly match the uploaded manifest, naming the field if validation fails. Both new uploads and identical reuploads return the typed plugin object from the response, including any extra summary fields. Version-conflict errors include a reminder to bump the manifest version when bricks, schemas, or endpoint URLs change, then redeploy.

const plugin = await uploadPluginManifest({ endpoint, apiKey, manifest });
console.log(`Registered ${plugin.slug} ${plugin.version} (${plugin.id})`);

Connections

The current manifest is unversioned. createPluginDefinition keeps serializable connection metadata together with its local runtime translator; only .manifest is uploaded. A brick slot references a type by key, and every type has a same-plugin validation brick with exactly one required slot.

const plugin = createPluginDefinition({
  slug: "issue-tools",
  name: "Issue tools",
  version: "1.0.0",
  visibility: "private",
  connectionTypes: [new BearerConnection("jira", {
    name: "Jira",
    validationBrick: "validate-jira",
  })],
  bricks: [
    {
      slug: "validate-jira",
      name: "Validate Jira",
      description: "Validate a Jira token.",
      endpointUrl: "https://plugins.example.com/issue-tools/validate-jira",
      input: z.object({}).strict(),
      output: z.object({ ok: z.boolean() }).strict(),
      connections: { credential: { typeKey: "jira", required: true } },
    },
    {
      slug: "create-issue",
      name: "Create issue",
      description: "Create a Jira issue.",
      endpointUrl: "https://plugins.example.com/issue-tools/create-issue",
      input: z.object({ title: z.string() }).strict(),
      output: z.object({ key: z.string() }).strict(),
      connections: {
        primary: { typeKey: "jira", required: true },
        fallback: { typeKey: "jira", required: false },
      },
    },
  ],
});

export const manifest = plugin.manifest;

Pass plugin to each connected createBrickHandler. The SDK redeems raw { credentials, config }, validates both halves with the local definition, runs its translator, and gives the translated string map to the handler as context.connections[slotName].

Triggers

Plugins can declare trigger definitions for provider events or schedules. Trigger definitions have no kind field; the builder converts their authored Zod config and output schemas to JSON Schema. A trigger may require one typed connection declared by the same plugin version.

const issueConfig = z.object({ projectKey: z.string() }).strict();
const issueEvent = z.object({ key: z.string(), title: z.string() }).strict();

const plugin = createPluginDefinition({
  slug: "issue-tools",
  name: "Issue tools",
  version: "1.0.0",
  visibility: "private",
  connectionTypes: [new OAuth2ClientCredentialsConnection("jira", {
    name: "Jira",
    validationBrick: "validate-jira",
    configSchema: z.object({
      tokenUrl: z.url(),
      scope: z.string().optional(),
    }).strict(),
    config: {
      tokenUrl: "https://auth.example.com/oauth/token",
      scope: "issues:read",
    },
  })],
  bricks: [{
    slug: "validate-jira",
    name: "Validate Jira",
    description: "Exchange and validate Jira client credentials.",
    endpointUrl: "https://plugins.example.com/validate-jira",
    input: z.object({}).strict(),
    output: z.object({ ok: z.boolean() }).strict(),
    connections: { credential: { typeKey: "jira", required: true } },
  }],
  triggers: [{
    slug: "issue-created",
    name: "Issue created",
    description: "Fires when an issue is created.",
    registerUrl: "https://plugins.example.com/triggers/register",
    unregisterUrl: "https://plugins.example.com/triggers/unregister",
    config: issueConfig,
    output: issueEvent,
    connection: { typeKey: "jira", required: true },
  }],
});

export const manifest = plugin.manifest;

createTriggerHandler discovers and briefly caches the environment configuration, verifies the short-lived lifecycle JWT, checks its trigger identity and operation, re-validates config with the supplied Zod schema, and dispatches to separate onRegister and onUnregister callbacks. When connection is declared, it redeems and translates that one connection through the discovered redemption URL and exposes only the translated string map in the callback context. Registration returns { publicEndpointUrl }; successful unregistration returns an empty 204 response.

const lifecycle = createTriggerHandler({
  plugin,
  triggerDefinitionId: "server-derived-definition-id",
  config: issueConfig,
  connection: { typeKey: "jira", required: true },
  async onRegister(input, { connection }) {
    // Persist input.ingestionSecret and its generation atomically before return.
    // Create or reuse provider state using connection, then return the stable route.
    return { publicEndpointUrl: "https://plugins.example.com/hooks/stable-route" };
  },
  async onUnregister(input, { connection }) {
    // Idempotently remove the provider registration and plugin-owned state.
  },
});

The callback boundary re-validates lifecycle config; it does not rely only on platform-side instance creation validation. Lifecycle bearer tokens, ingestion secrets, and redeemed credentials are never included in SDK error responses.

Use deliverTriggerEvent from the plugin service after provider authentication and normalization. ingestionBaseUrl is explicit deployment configuration—there is no hardcoded Taskflow hostname and no per-instance ingestion URL to store. The helper constructs /instances/{triggerInstanceId}/events, enforces the 1 KiB UTF-8 sourceEventId limit, sends the stored ingestion secret as the bearer, and throws a secret-safe TriggerEventDeliveryError for network or non-2xx failures.

TriggerInstanceStateStore is the optional storage adapter contract for plugin-owned state. Its atomic install operation accepts absent state or a newer credential generation, treats a byte-for-byte/logically equivalent same generation as unchanged, and rejects stale or contradictory generations without mutation. deleteIfUnchanged is also atomic. A bare eventually-consistent KV get/put pair does not implement these guarantees; this package intentionally ships no production storage adapter.

License

MIT