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

@omelhorsite/mcp

v0.2.0

Published

Model Context Protocol server exposing the omelhorsite SDK to agents, in the code-mode shape: two tools, and the SDK's own type declarations as the interface.

Readme

@omelhorsite/mcp

An MCP server for the omelhorsite API, built in the code-mode shape: instead of one tool per endpoint, it exposes two, and the model writes TypeScript against @omelhorsite/sdk.

search(query)   find the part of the API you need, as real type declarations
execute(code)   run TypeScript against the SDK and get the returned value back

The bet is Cloudflare's: a model writes better TypeScript than it writes tool calls, because it has read an enormous amount of the former and very little of the latter. Two things follow.

The types are the interface. There is no hand-written schema anywhere in this package: tsc emits .d.ts from packages/core, and search serves those declarations with their JSDoc intact. The JSDoc on this SDK documents rate limits, quota units, error shapes and the places the backend surprises you, so the model reads the same warnings a developer would.

The work happens in one round trip. "Find every file over 100 MB in my storage and total them up" is a loop. As seventy tools it is twenty turns of list, read, filter, call again. As one execute call it is nine lines that run once.


Read this before you install it

THIS IS NOT A SANDBOX.

execute runs the model's code in a bun subprocess on your machine, as you, with your credential. It is not a Cloudflare isolate. There is no strong security boundary against hostile code.

The subprocess bounds ACCIDENTS - a runaway loop, a program that never returns, output that would fill memory. It bounds nothing else. Code passed to execute can read and write your files, open sockets, await import("node:fs"), and read the token out of its own environment.

The trust model is exactly the trust model of giving an agent shell access. If you would not let this agent run bash, do not give it this server.

What is actually done about it, so the claim above stays precise:

| Measure | What it buys | What it does not | | --- | --- | --- | | Runs in a subprocess | a crash or a hang cannot take the server down | it is the same user, on the same machine | | Wall-clock deadline, then SIGKILL | no run wedges the client forever | a program can do damage in a second | | stdout/stderr piped, never inherited | a console.log cannot corrupt the JSON-RPC stream | nothing | | Minimal child environment | the server's other variables never cross | OMS_TOKEN does cross, by necessity | | The token never enters the program file | it is not in a world-readable temp file, or in a stack trace | the code can still read process.env | | Credential-shaped keys redacted from results | return oms does not put your token in a transcript | a program that wants to leak it, can | | Imports restricted to the SDK | a mistyped import fails with an explanation | await import(...) is not stopped |

Where a real sandbox goes

One function: runCode in src/runner/run.ts. Everything above it - the generated program text, the result protocol in src/runner/protocol.ts, both tools - is independent of how the code is evaluated.

Two replacements would give a real boundary:

  • the Dynamic Worker Loader (env.LOADER.get(id, () => ({ modules }))), which is what Cloudflare's own code mode uses: a V8 isolate per snippet, no filesystem, no ambient network, and outbound calls only through a binding the host controls;
  • the Sandbox SDK (getSandbox(env.Sandbox, id).exec(...)) when a full container is wanted instead.

Either one additionally makes the credential unreachable by the code that uses it: the snippet gets a request function, not the token. That is the property a subprocess fundamentally cannot provide, and it is why this file says what it says.


Configuring it in Claude Code

The server takes its credential from the environment. An MCP server has no TTY - no prompt, no browser, no follow-up question - so there is no interactive login and there will not be one.

.mcp.json

Project-scoped, checked in next to the repo (without the token):

{
  "mcpServers": {
    "omelhorsite": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/omelhorsite/apps/cli/packages/mcp/src/server.ts"],
      "env": {
        "OMS_TOKEN": "${OMS_TOKEN}"
      }
    }
  }
}

From the command line

claude mcp add omelhorsite \
  --env OMS_TOKEN="$OMS_TOKEN" \
  -- bun run /absolute/path/to/omelhorsite/apps/cli/packages/mcp/src/server.ts

Check it came up with /mcp inside Claude Code. The server writes its diagnostics to stderr; a client that shows MCP server logs will report oms-mcp: ready: 319 declarations, base https://backend.omelhorsite.pt.

Getting a token

Either credential the API accepts works, because they are indistinguishable on the wire:

  • an OAuth access token from the device flow, which is what oms auth login in @omelhorsite/cli produces. Ask for the narrowest scopes the agent needs: openid, then any of storage:read, storage:write, tools:read, tools:write, tickets:write. No scope grants administrative access.
  • a legacy session token, the opaque UUID the web app holds. It carries full account authority and never expires, which is exactly why it is the worse choice for an agent.

Leaving OMS_TOKEN unset is supported and the server says so on stderr. Short links, notepads, chests and IP lookup still work anonymously, at a smaller daily quota; anything account-shaped answers 401.

Environment

| Variable | Default | Meaning | | --- | --- | --- | | OMS_TOKEN | none | The credential. Absent means anonymous. | | OMS_BASE_URL | https://backend.omelhorsite.pt | API root. Point it at a local backend. | | OMS_MCP_TIMEOUT_MS | 60000 | Deadline for one execute, and its ceiling. Capped at 600000. | | OMS_MCP_MAX_OUTPUT_CHARS | 20000 | Per-section cap on what execute reports. | | OMS_MCP_BUN | the running bun | Override the binary the runner re-invokes. |


The two tools

search(query, limit?)

Returns declarations, not prose. Query it however is natural:

  • a question: "how do I shorten a URL"
  • a dotted path: "oms.storage.upload"
  • an exact type name: "CreateShortLinkInput"
  • empty: the map of the whole client

Ranking is weighted token overlap with two corrections that matter on real questions. Rarity, because url and id and create are in half the SDK and carry almost no signal while shorten is in three declarations. Coverage, because an entry matching every word of the query is answering it and one matching a single common word is a coincidence. Without either, "how do I shorten a URL" returns MultipartPartUrl.

execute(code, timeoutMs?)

Runs a snippet. oms is constructed and authenticated; every runtime export of the SDK is already in scope. await and return both work at the top level, because the snippet is wrapped in an async function.

The answer separates three things: the returned value, anything printed to stdout/stderr, and notes about what was truncated or removed. A thrown error comes back with the SDK's own detail - status, code, method, url, fieldErrors, retryAfterMs - which is what a model needs to decide between retrying, re-scoping and giving up.

Values that JSON cannot carry are tagged rather than lost: a Blob reports its size, a Uint8Array its length, a cycle is marked, a long listing is cut and the cut is stated.


Code the model writes

Real signatures from the current SDK. Each block is exactly what would go in the code argument.

Look something up.

const google = await oms.ipLookup.get("8.8.8.8");
return { asn: google.asn, org: google.organization, country: google.country };

Shorten a URL and read its statistics. Note the JSDoc that search returns for create: ten creations per hour per IP, for everyone, signed in or not.

const link = await oms.shortLinks.create({ url: "https://example.com", endpoint: "demo" });
const stats = await oms.shortLinks.stats(link.id);
return { url: oms.shortLinks.shortUrl(link), clicks: stats.total_clicks };

Walk a folder tree and find what is big. This is the case the whole design exists for: a loop, a pager and a filter, in one call instead of twenty turns.

const roots = await oms.storage.roots();
if (roots.home === null) return "no home directory";

const big: { name: string; mb: number }[] = [];
const queue: string[] = [roots.home];

while (queue.length > 0 && big.length < 20) {
  const parentId = queue.shift()!;
  const children = await collect(await oms.storage.list({ parentId, pageSize: 500 }), 5000);

  for (const node of children) {
    if (node.kind === "directory") queue.push(node.id);
    else if (node.size > 100_000_000) big.push({ name: node.name, mb: Math.round(node.size / 1e6) });
  }
}

return big.sort((a, b) => b.mb - a.mb);

Run a metered tool and wait for it. Check the quota first; the unit differs per tool (seconds of media for the audio and video tools, edits for jumpstyle).

const quota = await oms.tools.transcription.quota();
if (!quota.unlimited && (quota.remaining_seconds ?? 0) < 60) {
  return { skipped: "not enough daily quota left", quota };
}

const audio = await oms.storage.download("<fs node id>");
const done = await oms.tools.transcription.run({
  audio: file(audio.data, audio.filename ?? "audio.m4a"),
  language: "pt",
});

return { status: done.status, text: done.text };

Local helpers, no network and no credential.

return {
  passphrase: generatePassphrase({ words: 5 }),
  strength: passwordStrength(generatePassword({ length: 20 })),
};

Regenerating the types

Run this after any change to packages/core/src:

bun run --filter '@omelhorsite/mcp' build:types

scripts/generate-types.ts runs tsc over the SDK, then parses the emitted declarations with the TypeScript compiler API and writes three things into generated/:

| File | What it is | | --- | --- | | sdk/**/*.d.ts | raw tsc output, one file per SDK module | | catalog.json | the search index: declarations, summaries, members, runtime exports | | sdk.d.ts | every declaration in one file, for a human to read |

generated/ is committed on purpose. An MCP client launches this server without running a build first, and a server that needs one fails on the first call, in a place nobody is watching.

The cost of committing it is that it can go stale, so the catalog records the byte size of every SDK source and the server compares them at start-up. A mismatch prints a warning naming the changed files. It is a size check, not a hash: it catches "somebody edited the SDK and forgot to regenerate", which is the failure that happens, and misses an edit that preserves the length, which is not.


Layout

scripts/
  generate-types.ts       tsc -> .d.ts -> catalog.json      (build time)
  tsconfig.declarations.json
src/
  server.ts               the bin: wires the two tools onto stdio
  config.ts               the only module that reads process.env
  paths.ts                everything located from import.meta.url
  catalog.ts              the search index and its scoring (no I/O)
  catalogFile.ts          loading it, and noticing when it is stale
  host.d.ts               the host globals this package adds to types/host.d.ts
  tools/
    search.ts             ranking, budgeting and rendering declarations
    execute.ts            the tool description and the answer format
  runner/
    run.ts                spawn, deadline, capture      <- the sandbox seam
    program.ts            snippet -> program file
    harness.ts            the child side: builds the client, reports the result
    encode.ts             values JSON cannot carry
    protocol.ts           the one shape both sides agree on
generated/                committed build output (see above)
test/

src/host.d.ts merges Bun.spawn, BunFile.delete and process.execPath into the workspace's shared types/host.d.ts rather than editing it, because that file belongs to the workspace and this package's needs are its own. Both go away when @types/bun is approved as a dependency.


Limitations

These are known and unfixed, not oversights.

No sandbox. Said at length above. It is the only one that changes what you should be willing to point this at.

Snippets are not typechecked. bun strips TypeScript without checking it, so a type error in the model's code surfaces as a runtime failure - or, worse, does not surface at all when the annotation was wrong but the value happened to work. Running tsc over the snippet against generated/sdk before spawning would catch this, at roughly a second per call, and is a clean addition to runner/program.ts.

No streaming and no progress. A tool result arrives whole, at the end. A five-minute transcription is silent for five minutes and then answers, so budget timeoutMs for it. MCP progress notifications would fix this and the runner would need to forward the SDK's onProgress callbacks out of the subprocess.

One process per call. Roughly 40 ms of bun start-up per execute, and no state survives between calls: a variable set in one snippet does not exist in the next. Ids, tokens and handles must be returned and passed back in. This is deliberate - a persistent interpreter is a much larger surface and a much stranger failure mode - but it is a real cost on a multi-step task, which is an argument for writing one bigger snippet.

Search is lexical. Token overlap with IDF and coverage weighting, no embeddings. It is good on nouns from the API and weaker on synonyms the SDK does not use: "transcribe" does not stem to "transcription", so that query ranks the captions tool first. The fallback is to search the exact name.

Declarations carry their private members. tsc emits private readonly children; lines because they matter for structural typing, and they are served as-is. Trimming them would mean the text no longer matches what the compiler produced, which is a worse trade than the tokens they cost.

The catalog staleness check is a size comparison. See above.

Paths assume the package runs from source. src/paths.ts locates generated/ and packages/core relative to itself. Bundling src/ into a dist/ moves it, and those constants have to move too.