Skip to content

Plugins

@dappql/mcp auto-discovers any npm package in your project's node_modules that ships a DappQL manifest. Those packages become plugins: their contracts, ABIs, addresses, and protocol metadata are exposed to your AI agent alongside the local project's contracts.

Install a plugin:

bash
pnpm add @underscore/dappql

Restart your MCP client. That's it. The agent now has typed access to 75+ contracts from Underscore Finance in addition to whatever lives in your own dapp.config.js.

How discovery works

On startup, @dappql/mcp walks node_modules/ looking for any package.json with a top-level dappql field (what that field looks like). Both flat (node_modules/@foo/bar) and pnpm-hoisted layouts are supported. Transitive installs count — if a plugin sneaks in through a dep's dep, the agent sees it.

The server logs discovered plugins at startup:

[@dappql/mcp] Project: /path/to/your/dapp.config.js
[@dappql/mcp] Chain: 8453
[@dappql/mcp] Contracts: 4
[@dappql/mcp] Plugins: 2 — @underscore/dappql@1.0.0 (75), @aerodrome/dappql@0.3.0 (12)
[@dappql/mcp] Writes: disabled — ...

"75" and "12" are contract counts per plugin.

The source field

Every tool that returns or accepts contracts now carries a source:

  • "project" — your local dapp.config.js
  • The plugin's package name — e.g. "@underscore/dappql"

Example listContracts output with a plugin installed:

json
{
  "chainId": 8453,
  "projectContracts": 3,
  "plugins": [
    { "name": "@underscore/dappql", "version": "1.0.0", "chainId": 8453, "contractCount": 75 }
  ],
  "contracts": [
    { "source": "project", "name": "MyToken", "shape": "singleton", "address": "0x…", "readCount": 8, "writeCount": 3, "eventCount": 2 },
    { "source": "@underscore/dappql", "name": "Ledger", "shape": "singleton", "address": "0x…", "readCount": 14, "writeCount": 0, "eventCount": 4 },
    { "source": "@underscore/dappql", "name": "UndyUsd", "shape": "singleton", "address": "0x…", "readCount": 22, "writeCount": 5, "eventCount": 6 }
  ]
}

Disambiguating contract names

When a project and a plugin both declare a contract with the same name (say, Token), the agent must pick a source:

agent: getContract({ name: "Token" })
→ Error: Ambiguous contract name "Token" — exists in: project, @underscore/dappql.
  Pass the `source` argument to disambiguate.

agent: getContract({ name: "Token", source: "@underscore/dappql" })
→ { "source": "@underscore/dappql", "name": "Token", "reads": [...], ... }

Chain tools (callRead, multicall, simulateWrite, callWrite, getEvents) accept the same source argument. A multicall can mix project + plugin contracts in a single batch:

json
{
  "calls": [
    { "key": "myBalance",    "contract": "MyToken",  "source": "project",              "method": "balanceOf", "args": ["0x…"] },
    { "key": "undyBalance",  "contract": "UndyUsd",  "source": "@underscore/dappql",   "method": "balanceOf", "args": ["0x…"] }
  ]
}

All routed through one RPC batch.

Plugin metadata

listPlugins returns everything @dappql/mcp knows about each installed plugin:

json
{
  "total": 1,
  "plugins": [
    {
      "name": "@underscore/dappql",
      "version": "1.0.0",
      "chainId": 8453,
      "protocol": {
        "name": "Underscore Finance",
        "website": "https://underscore.finance",
        "docs": "https://docs.underscore.finance",
        "explorer": "https://basescan.org",
        "repo": "https://github.com/underscore-finance/typescript-sdk"
      },
      "contracts": ["AddyRegistry", "Ledger", "MissionControl", "UndyUsd", /* … */],
      "hasAgentsDoc": true
    }
  ]
}

The protocol.docs URL is a hint for the agent: if it needs to reason about the protocol beyond what's in the ABIs, it can WebFetch the docs. protocol.repo points to the source, protocol.explorer gives it a place to look up on-chain activity.

Transaction decoding

getTransaction decodes function calls and event logs against every known ABI — project first, then plugins. Each decoded entry tags its source, so you can tell whether a log was emitted by a contract in your project or in a plugin's registry.

Useful when analyzing txs that touch multi-protocol flows: an Underscore wallet operation routing through Aerodrome Swap logs decodes both sides automatically if both plugins are installed.

Addresses and dynamic resolution

Plugins ship an addresses.json with whatever static addresses the publisher knew at pack time. Template contracts and dynamically resolved contracts have undefined entries.

When the agent calls a plugin contract with no known address, the MCP raises a clear error: pass an address argument, or check the plugin's docs for how addresses resolve. Agents can also introspect the plugin's contracts to find a registry contract (AddressProvider, Addys, etc.) and resolve via a callRead.

This is intentional. Baking a static resolver into every published artifact would force republishing on every registry change; leaving resolution live means agents always see the current chain state.

Publishing your own plugin

See Publishing as an npm plugin for the publisher side. The short version:

js
// dapp.config.js
package: {
  name: '@myprotocol/dappql',
  version: '1.0.0',
  protocol: {
    name: 'MyProtocol',
    docs: 'https://docs.myprotocol.xyz',
  },
}

Then:

bash
npx dappql pack
cd dappql-package
npm publish --access public

Anyone installing your package and running @dappql/mcp immediately gives their agent typed context for your protocol.