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

sqemo-mcp

v2.8.1

Published

MCP server for Sqemo - query and edit ERDs with your team's naming standards

Downloads

3,280

Readme

sqemo-mcp

A local stdio MCP server that lets AI agents read and edit Sqemo ERDs. It works with both local .erd.json files and ERDs saved on the Sqemo server, and it can generate and check physical names against your team's word list and naming rules.

  • Node >= 22 (supabase-js requires native WebSocket — server tools and login do not work on Node 20)
  • Every local-file tool works without logging in. Only the server tools require login.

v2.0.0 — breaking changes (single-model ERDs)

Sqemo's ERD model merged its separate logical/physical representation into a single entity/attribute model. This is transparent for old .erd.json files (they migrate on load), but it changes the MCP surface:

  • No more physical table/column ids. Every tool now returns a single entityId/ attributeId — there was never a separate physical id exposed by these tools, so most integrations are unaffected. The one exception: any id you cached from a table or column concept elsewhere no longer resolves — always re-fetch ids from list_entities/ get_entity within the current session rather than persisting them across sessions.
  • auto_layout dropped the view parameter. Logical and physical placement are now the same single layout, so view: "logical" | "physical" | "both" had nothing left to distinguish. Calls that still send view are ignored (the field is simply gone from the schema).
  • upsert_entity/upsert_attribute/get_entity/list_entities responses are unchanged in shape (physicalName is still returned) but are now read directly from the entity/ attribute instead of a paired physical table/column — no action needed by callers.

Install & register

Claude Code (.mcp.json)

{
  "mcpServers": {
    "sqemo": { "command": "npx", "args": ["-y", "sqemo-mcp"] }
  }
}

For local development (working from a checkout of this repository):

{
  "mcpServers": {
    "sqemo": { "command": "node", "args": ["./mcp/dist/index.js"] }
  }
}

Server tools (list_erds, etc.) need either a populated DEFAULT_SUPABASE_ANON_KEY in src/auth/defaults.ts or the ERDMAKER_SUPABASE_ANON_KEY environment variable (otherwise they return not_configured). Local-file tools work without any of this.

Claude Desktop

Add the same mcpServers entry to the config file. Claude Desktop has no project working directory, so use an absolute path instead of ./mcp/... when registering a local build.

  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Codex CLI

codex mcp add sqemo -- npx -y sqemo-mcp

Or add the equivalent TOML to ~/.codex/config.toml:

[mcp_servers.sqemo]
command = "npx"
args = ["-y", "sqemo-mcp"]

Most other MCP clients (Cursor, etc.) use the same mcpServers JSON as Claude Code.

Login (for server ERDs)

npx sqemo-mcp login    # pick Google, GitHub, or email + password
npx sqemo-mcp logout   # remove stored credentials

login asks how you want to sign in. Options 1 and 2 (Google / GitHub) open a browser tab, complete a PKCE OAuth flow, and hand the session back through a one-shot loopback server on 127.0.0.1; option 3 takes an email and password in the terminal. Only a refresh token is ever stored.

Before the npm package is available (local development), use node mcp/dist/index.js login / logout.

  • Credentials are stored in ~/.erdmaker/credentials.json (mode 0600 on POSIX); your password itself is never stored.
  • Several sqemo-mcp processes can share that file (e.g. Claude Code and Codex connected at the same time, or the CLI run while an agent's server is up). Refresh tokens rotate on every refresh, so each process re-reads the file before using its cached session and adopts a token another process rotated (2.8.1+). If you still see not_authenticated everywhere at once, run npx sqemo-mcp login again.
  • Non-interactive shortcuts (no menu): --password forces the email + password path, --provider google|github forces a browser path.
  • Piped input skips the menu and goes straight to email + password, so existing automation keeps working: printf 'email\npassword\n' | npx sqemo-mcp login
  • The browser paths need a browser on the same machine (the callback returns to 127.0.0.1). Over SSH, use --password or the SQEMO_EMAIL / SQEMO_PASSWORD environment variables.

Environment variables

| Variable | Purpose | |----------|---------| | ERDMAKER_HOME | Override the credentials directory (default ~/.erdmaker) | | ERDMAKER_SUPABASE_URL | Override the Supabase URL (default: the Sqemo production server) | | ERDMAKER_SUPABASE_ANON_KEY | Override the Supabase anon (publishable) key | | ERDMAKER_MAX_REQUESTS_PER_MINUTE | Per-minute cap on Supabase requests (default 120, 0 disables) | | ERDMAKER_MAX_REQUESTS_PER_DAY | Daily cap on Supabase requests (default 10000, 0 disables). Tracked in ERDMAKER_HOME/usage.json |

If the connection settings are empty, server tools return a not_configured error.

The two caps are a safety net against an agent stuck in a loop flooding requests. When exceeded, tools fail with a rate_limited error that instructs the agent to stop and notify the user. For intentional bulk work, raise the caps via the environment variables above or set them to 0.

CLI (for CI pipelines)

Independent of the MCP server, two offline, file-based subcommands are available (no login required).

# Check naming standards — exits with code 1 on violations, failing CI
npx sqemo-mcp lint schema.erd.json

# Export the schema — result goes to stdout
npx sqemo-mcp export schema.erd.json --format sql --dialect postgres > schema.sql
npx sqemo-mcp export schema.erd.json --format dbml > schema.dbml
  • lint: checks logical names for unregistered words (missing from the word list) and physical-name drift (mismatch with the standard-generated name; manually-overridden physical names and FK attributes excluded). Exit codes: 0 = clean, 1 = violations, 2 = argument/file error. Violations and the summary go to stderr. The unregistered-word check only runs when namingRules.unknownWordMode === "warn"; projects set to keep/romanize do not flag unregistered words.
  • export: --format sql|dbml (default sql); for sql, pick a dialect with --dialect (defaults to the project dialect). Output on stdout, errors on stderr (exit code 2).

GitHub Actions example:

- run: npx sqemo-mcp lint schema.erd.json

Drift check in CI (Pro)

# .github/workflows/schema-drift.yml
- run: npx sqemo-mcp lint erd/app.erd.json --db "$DATABASE_URL" --ignore "flyway_*"
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}   # read-only account recommended
    SQEMO_EMAIL: ${{ secrets.SQEMO_EMAIL }}     # Sqemo Pro account (password sign-in;
    SQEMO_PASSWORD: ${{ secrets.SQEMO_PASSWORD }} # OAuth-only accounts: set a password first)

No database credentials ever reach Sqemo servers — introspection runs inside your CI job and queries only information_schema. Prefer zero credentials in CI? Compare against a dump instead: npx sqemo-mcp lint erd/app.erd.json --schema schema.sql --dialect postgres.

The source parameter

Most read/write tools take the target ERD as source — specify exactly one of the two (passing both is rejected at the schema level).

  • { "file": "C:/path/to/model.erd.json" } — a local file
  • { "erdId": "<uuid>" } — a server ERD (get ids from list_erds)

Exception: the three dictionary lookup tools (search_dictionary · check_naming · generate_physical_name) also accept { "workspaceId": "<uuid>" } — they check against the latest workspace-standard master (word list + naming rules), not a project's cached copy. Get ids from list_workspaces. Write tools do not accept the workspaceId source (lookup only).

diff_erds additionally accepts {sql, dialect?} and {dbml} text sources for before/after — useful as a dry run before a full-replace import_sql/import_dbml.

Workspace standard lookup

You can query and check your team's naming standard directly, without a project:

list_workspaces
→ [{ "workspaceId": "…", "name": "Our Team", "role": "owner",
     "glossary": { "version": 7, "updatedAt": "…" } }]

check_naming { source: { workspaceId: "…" }, logicalName: "member number" }
→ checked against the latest workspace-standard master (not a project cache)

glossary: null means the workspace has no standard yet (create one by linking a standard in the web app). Editing the standard master directly is not supported, but you can request new words through the proposal queue: submit with propose_dictionary_word → the owner approves/rejects in the web app → check status with list_proposals, withdraw with withdraw_proposal. Proposal sources accept {workspaceId} or a standard-linked ERD ({file}/{erdId}). Submitting and listing proposals is workspace-member only — a shared editor of a standard-linked ERD who is not a member gets no_permission.

Starting a project on your team's standard

A new ERD has an empty word list, so physical names come out as the logical names verbatim. Pass workspaceId to create_erd to bind it to the standard from the start — the word list, naming rules and domains are pulled in at creation, so every name you add follows the team standard:

list_workspaces
create_erd { name: "Threaded Board", workspaceId: "…", target: { server: true } }
upsert_entity { source: { erdId: "…" }, logicalName: "Post" }
// → physical name resolved through the standard's word list

Linking pulls the standard's word list, naming rules and domains, but it does not regenerate physical names that already exist — when you combine workspaceId with sql/dbml, the parsed names are kept as-is and a warning says so. Standard-linked dictionaries and naming rules are read-only from MCP (dictionary_is_linked); edit them on the master in the web app, or propose words with propose_dictionary_word.

Tools (39)

Read

| Tool | Description | |------|-------------| | list_erds | List server ERDs (role: owner/editor, shared). Owner and shared editors can both write. Requires login | | list_workspaces | List my workspaces (owner first) + whether a standard (word list + naming rules) exists. Requires login | | get_erd_overview | Name, dialect, entity/relationship/domain/word-list stats, subject areas, standard link status | | list_entities | List entities (logical name, physical name, attribute count) | | get_entity | Entity detail (attributes, keys, domains, logical/physical mapping). Prefers entityId; falls back to exact logicalName match | | list_relationships | List relationships (endpoint entity names, cardinality, identifying flag) | | list_domains | List domain definitions (name, data type). Accepts the {workspaceId} source (standard domain dictionary) | | search_dictionary | Partial-match word-list search (logical/physical/abbreviation/English/synonyms, max 50). Accepts {workspaceId} | | check_naming | Check a logical name against the naming standard (+ optional physicalName comparison). Accepts {workspaceId} | | generate_physical_name | Logical name → physical name (unregistered words reported in warnings). Accepts {workspaceId} | | export_sql | Generate CREATE TABLE SQL — 7 dialects (mysql/postgres/cubrid/oracle/sqlserver/sqlite/h2) | | export_dbml | Generate DBML text | | validate_erd | Validate the project ({ valid, errors }) | | lint_erd | Full project lint — structure (PK, attributes, descriptions, domains), referential integrity, duplicate physical names (incl. case-only collisions), duplicate logical entity names, identifier length limits (dialect-aware, tables/columns/indexes/FK constraints), auto-increment on non-integer types (dialect-aware), naming standard (unregistered words, drift) | | diff_erds | Compare two sources (unchanged omitted) — before/after each accept {file}, {erdId}, {sql, dialect?}, {dbml}. Dry run before imports. Summary is entity-based | | check_db_drift | Check drift between the ERD and a live database or schema dump (Pro) | | list_proposals | List the workspace-standard proposal queue (word/domain, mine flag, status filter). Requires login |

Write

| Tool | Description | |------|-------------| | create_erd | Create a new ERD, empty or from SQL/DBML text — target: {file} or {server:true}; a given dialect becomes the project dialect; workspaceId links it to that workspace's standard on creation | | upsert_entity / delete_entity | Create/update/delete entities (physical name derived automatically) | | upsert_attribute / delete_attribute | Edit attributes — setting PK forces nullable=false and propagates to child FKs; with a domain, the domain decides the data type; defaultValue is raw SQL, so quote literals yourself ('N', not N) | | upsert_attributes | Bulk form of upsert_attribute — many attributes across any entities in one load/save, applied in order, all-or-nothing. Prefer it for anything beyond a handful (e.g. linking hundreds of imported columns to domains) | | upsert_relationship / delete_relationship | Edit relationships (FKs derived automatically; specify cardinality or ends; onDelete/onUpdate referential actions and constraintName supported — noAction/empty string revert to defaults) | | upsert_domain / delete_domain | Edit domain definitions (existing description preserved when omitted) | | upsert_domains | Bulk form of upsert_domain — many domains in one load/save, all-or-nothing. Use this instead of looping upsert_domain: each single call is several server requests, so a loop burns the per-minute cap ~4× faster than it looks | | upsert_dictionary_word / delete_dictionary_word | Edit the word list — rejected for standard-linked (glossaryLink) dictionaries | | upsert_dictionary_words | Bulk form of upsert_dictionary_word — many entries in one load/save, all-or-nothing, same glossaryLink gate. Prefer it whenever adding more than a handful of words (e.g. a word list reverse-engineered from an imported schema) | | update_naming_rules | Partially edit naming rules (delimiter, case, unknown-word handling, selfReferencePrefix). Existing physical names are not regenerated retroactively (noted in warnings) — set the rules before modelling. Rejected for standard-linked ERDs | | import_sql / import_dbml | Fully replace an existing ERD with the parse result (ids preserved). Rejected when zero tables | | introspect_db | Import a live PostgreSQL/MySQL schema into an ERD (read-only, Pro) | | auto_layout | Auto-arrange entities (dagre) — single shared layout (no separate logical/physical placement). Reference copies, notes, and waypoints preserved | | propose_dictionary_word | Propose a word for the standard's word list (owner approves in the web app). Duplicates: already_exists / already_proposed | | withdraw_proposal | Withdraw your own pending proposal (others' or processed ones: not_found) |

Self-referencing relationships

A self-referencing FK cannot reuse the PK's name, so it gets a role prefix. The prefix defaults to Parent, so a self-referencing FK on Post comes out as PARENT_POST_NO with no configuration. The prefix is resolved through the word list like any other word — register it to get an abbreviation:

// 1. register the word so the prefix gets its physical form / abbreviation
upsert_dictionary_word { logicalWord: "Parent", physicalWord: "PARENT", abbreviation: "PRNT" }
// 2. set the rule if you want a different prefix (not applied retroactively — do this first)
update_naming_rules   { selfReferencePrefix: "Parent" }
// → a self-referencing FK on "Post" becomes "Parent Post Number" / PRNT_POST_NO

When a project created before this default changed loads, an unset prefix is written into its naming rules: 상위 if it already has a self-referencing relationship — so its existing column names never shift — and Parent if it does not.

Server write rules

Writing to a server ERD is allowed for the owner or a shared editor. Concurrent saves on shared ERDs go through CAS 3-way auto-merge; merge conflicts return save_conflict. Saves are protected by a version CAS — if another session saved first, save_conflict is returned. Standard-linked dictionaries stay locked to the standard master (dictionary_is_linked). When an auto-merge pulls in another session's word list changes, write-tool results include a note in warnings (e.g. existing physical names are not regenerated automatically). Every write-tool result always includes warnings: string[].

Error codes

Every tool error is returned as { code, message } — handle errors by code.

| code | Meaning | |------|---------| | not_configured | Supabase connection settings missing | | not_authenticated | Login required (npx sqemo-mcp login) | | no_permission | Write attempted on a server ERD where you are neither owner nor shared editor | | save_conflict | Version CAS conflict — re-fetch and retry | | not_found | Target (file/ERD/entity/attribute/…) not found | | dictionary_is_linked | Attempted to edit a standard-linked dictionary | | invalid_project_file | Not a valid .erd.json | | validation_failed | Project/input validation failed | | unsupported_dialect | Unsupported SQL dialect | | invalid_source | Invalid source/input combination (empty SQL, zero-table import, …) | | already_exists | The proposed word is already in the standard word list | | already_proposed | A pending proposal for the same word already exists | | plan_limit_exceeded | Plan quantity limit reached (cloud projects, snapshots, members, …) or a Team-only feature | | rate_limited | Requests exceeded the per-minute/daily cap — the agent may be stuck in a loop; stop and notify the user. For intentional bulk work, adjust the caps via environment variables | | internal_error | Unexpected internal failure (anything not covered above) |

Development

cd mcp
npm install
npm test        # vitest
npm run lint    # tsc --noEmit
npm run build   # tsup → dist/index.js

Releasing

  1. Bump the version in package.json, server.json (two places) and package-lock.json
  2. npm run builddist/ must carry the new tool schemas
  3. npm publish (interactive auth, so a human runs it)
  4. GITHUB_TOKEN=<classic PAT, read:org> node scripts/registry-publish.js, then revoke the PAT

Step 4 publishes to the MCP Registry and re-queries isLatest to confirm. The script header records the traps (classic PAT is required for org-owned servers; the publish body is the raw server.json, not the shape the registry returns on read).