| name | generate-skill |
| description | Generate a reusable skill from a Solana Anchor program IDL. Takes a program address, fetches the on-chain IDL, and outputs a ready-to-use skill with Phantom MCP integration for transaction execution. |
Solana Skill Generator
Generates a skill file from any Solana Anchor program with a published IDL. The generated skill gives AI agents the ability to interact with that program's instructions using Phantom MCP for signing and sending transactions.
When to Use
When a developer wants to build an MCP skill for a Solana program and says something like:
- "generate a skill for [program address]"
- "create a skill from this program"
- "run generate-skill for [address]"
How to Run
Step 1 — Get the program address
If the user hasn't provided a program address, ask for it. Accept mainnet program addresses only (for v1).
Step 2 — Fetch the IDL
Run these commands in sequence using Bash. The setup step is self-contained — it creates an isolated temp directory with all dependencies so this works in any session.
Setup (run once per session):
mkdir -p /tmp/phantom-idl-fetch && cd /tmp/phantom-idl-fetch && echo '{"type":"module"}' > package.json && npm install @coral-xyz/anchor@0.29.0 @coral-xyz/anchor-new@npm:@coral-xyz/anchor@latest @solana/web3.js @solana/spl-token --save-quiet && echo "Dependencies ready"
Why two anchor versions? No single version handles both IDL formats:
@coral-xyz/anchor@0.29.0 — works with legacy IDLs (Anchor < 0.30) but crashes on new-format IDLs
@coral-xyz/anchor-new (alias for latest) — works with new IDLs (Anchor ≥ 0.30) but crashes on legacy IDLs
The fetch script uses 0.29.0 (its fetchIdl works for both). After fetching, detect the format and use the right package in generated build scripts.
Fetch script — save as /tmp/phantom-idl-fetch/fetch-idl.mjs:
import { Program } from "@coral-xyz/anchor";
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
const PROGRAM_ADDRESS = process.argv[2];
if (!PROGRAM_ADDRESS) { console.error("Usage: node fetch-idl.mjs <program_address>"); process.exit(1); }
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const programId = new PublicKey(PROGRAM_ADDRESS);
const idl = await Program.fetchIdl(programId, { connection });
if (!idl) {
console.log("NO_IDL_FOUND");
process.exit(0);
}
console.log(JSON.stringify(idl, null, 2));
Run it:
cd /tmp/phantom-idl-fetch && node fetch-idl.mjs <PROGRAM_ADDRESS>
If NO_IDL_FOUND: Tell the developer clearly — "No IDL found for this program. This usually means it's a native program or an Anchor program that hasn't published its IDL on-chain. Programs without a published IDL are not supported in v1." Do not continue.
Step 3 — Check for Phantom MCP and catalog available tools
Check whether get_connection_status (from @phantom/mcp-server) is available as a tool in the current session.
- If not available: Include a prominent setup block in the generated skill recommending Phantom MCP installation.
- If available: Note which tool categories are present so the generated skill uses the right tool for each instruction type.
Phantom MCP exposes 27 tools. Generated skills should prefer the highest-level tool that fits the instruction rather than always building raw transactions. Use this reference when deciding what to generate:
Always use (every action):
| Tool | When |
|---|
get_connection_status | First — confirm the user is authenticated before doing anything |
get_wallet_addresses | Get the user's Solana (and EVM) address — fee payer and default signer |
simulate_transaction | Before every send_solana_transaction — catch errors before they hit chain |
Prefer over raw transactions when the instruction maps cleanly:
| Tool | Use instead of a raw tx when... |
|---|
transfer_tokens | The instruction is a simple SOL or SPL token transfer |
buy_token | The instruction is a token swap on Solana |
pay_api_access | Collecting a CASH payment from the user |
sign_solana_message | The instruction only requires message signing, not a transaction |
send_evm_transaction | The program targets an EVM chain (Base, Polygon, Ethereum, Monad) |
sign_evm_personal_message | EVM personal message signing |
sign_evm_typed_data | EVM typed data (EIP-712) signing |
Read before acting (use to inform the user or validate inputs):
| Tool | Use when... |
|---|
get_token_balances | Instruction involves spending tokens — verify the user has enough first |
get_token_allowance | EVM instruction requires a token approval check |
get_perp_account | Instruction interacts with a perp account — fetch current state first |
get_perp_markets | User needs to pick a market — list available ones |
get_perp_positions | Show open positions before modify/close actions |
get_perp_orders | Show open orders before cancel actions |
get_perp_trade_history | User asks about past trades |
Perp-specific (use directly for perpetuals programs — do not build raw txs for these):
| Tool | Use for... |
|---|
open_perp_position | Opening a leveraged long/short |
close_perp_position | Closing an open position |
cancel_perp_order | Cancelling a limit order |
update_perp_leverage | Changing leverage on a position |
deposit_to_hyperliquid | Depositing collateral |
transfer_spot_to_perps | Moving tokens from spot to perp margin |
withdraw_from_perps | Withdrawing collateral |
portfolio_rebalance | Rebalancing a perp portfolio |
Auth:
| Tool | Use when... |
|---|
phantom_login | User is not authenticated — trigger the login flow |
Step 4 — Parse the IDL
Anchor has two IDL formats. Detect which one you have before parsing:
Legacy format (Anchor < 0.30) — top-level name field:
{ "version": "0.1.0", "name": "my_program", "instructions": [...] }
New format (Anchor 0.30+) — top-level address and metadata fields:
{ "address": "ABC...", "metadata": { "name": "my_program", "version": "0.2.0" }, "instructions": [...] }
Extract the following, handling both formats:
| Field | Legacy | New format |
|---|
| Program name | idl.name | idl.metadata.name |
| Version | idl.version | idl.metadata.version |
| Program address | use the address passed by the user | idl.address |
For each instruction, extract:
name — instruction name
args — list of { name, type } parameters
- Legacy:
args[].type is a string or object
- New format:
args[].type follows the same shape but may use { defined: { name: "TypeName" } } instead of { defined: "TypeName" }
accounts — list of { name, writable?, signer?, pda? }
- Legacy uses
isMut / isSigner; new format uses writable / signer
- Normalize to
writable / signer when generating code
docs — description if present (array of strings — join with space)
Account filtering — exclude from user-facing docs (but keep in code):
Skip accounts that match these patterns (they are boilerplate):
systemProgram, tokenProgram, associatedTokenProgram
rent, clock, sysvarInstructions
programId, any account ending in Program
- PDAs derived by the program itself (where
pda is defined in the IDL)
Type mapping — convert Anchor types to human-readable:
| Anchor | Description |
|---|
u64, u128, i64 | number (use lamports/basis points as appropriate) |
publicKey / pubkey | Solana wallet or token account address |
bool | true/false |
string | text |
{ defined: "TypeName" } or { defined: { name: "TypeName" } } | reference the IDL's types array to expand |
{ vec: T } | array of T |
{ option: T } | optional T |
Step 5 — Generate the skill file
Write the skill file to <skills-dir>/<program-name>/SKILL.md in the current working directory. Create <skills-dir>/<program-name>/ if it doesn't exist. If the platform-specific skills directory is unknown, use skills/ as the default.
For the program name use idl.name (legacy) or idl.metadata.name (new format); for the address use idl.address (new format) or the address passed by the user (legacy). Use this structure:
---
name: <program-name-kebab-case>
description: Interact with <Program Name> on Solana. <One sentence from IDL metadata or derived from program name.>
---
# <Program Name> Skill
Enables AI agents to interact with [<Program Name>](<explorer link>) (program: `<ADDRESS>`).
<If Phantom MCP NOT available in session:>
## ⚠️ Setup Required: Phantom MCP
This skill uses Phantom MCP to sign and send transactions. Install it before using this skill:
**Add to your MCP config (platform-specific path, e.g. `claude_desktop_config.json`):**
```json
{
"mcpServers": {
"phantom": {
"command": "npx",
"args": ["-y", "@phantom/mcp-server@latest"]
}
}
}
On first use, you'll be prompted to authenticate with Google or Apple.
Helper: Fetch Pool State (if needed)
If any instruction requires PDAs or accounts that can be derived from on-chain state (e.g. vaults, oracles, tick arrays), add a ## Helper section before the actions with a script that fetches the program's state account and derives those addresses. This removes the need for the developer to look them up manually.
Pattern (use @coral-xyz/anchor-new for new-format IDLs, @coral-xyz/anchor for legacy):
import anchor from "@coral-xyz/anchor-new";
const { Program, AnchorProvider } = anchor;
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("<PROGRAM_ID>");
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const provider = new AnchorProvider(connection, { publicKey: PublicKey.default }, {});
const idl = await Program.fetchIdl(PROGRAM_ID, provider);
const program = new Program(idl, provider);
const state = await program.account.<stateName>.fetch(new PublicKey(process.argv[2]));
console.log(JSON.stringify({ }, null, 2));
Only add this section if the IDL has accounts that are clearly derivable (vaults, PDAs with known seeds, state-owned accounts). If all required accounts must be supplied by the user, omit this section.
Available Actions
<For each instruction, one section:>
<docs string if present, or a generated one-sentence description based on instruction name and accounts>
What to ask the user for:
<arg name>: —
<account name>: Solana address —
To execute:
Before writing the execution steps, check the tool reference table in Step 3. If this instruction maps to a high-level Phantom MCP tool (transfer_tokens, buy_token, open_perp_position, etc.), use that tool directly and skip the raw transaction script. Only build a raw transaction when no high-level tool fits.
If using a high-level Phantom MCP tool:
- Call
get_connection_status — if not authenticated, call phantom_login first.
- Call
get_wallet_addresses to get the user's Solana address.
- If the instruction spends tokens, call
get_token_balances and confirm the user has sufficient balance before proceeding.
- Call the appropriate tool directly (e.g.
transfer_tokens, buy_token, open_perp_position).
If building a raw Anchor transaction:
- Call
get_connection_status — if not authenticated, call phantom_login first.
- Call
get_wallet_addresses to get the user's Solana address.
- If the instruction spends tokens, call
get_token_balances and confirm sufficient balance before proceeding.
- Ensure setup has been run (see Step 2 of this skill). Save and run the script from
/tmp/phantom-idl-fetch/ so it picks up the installed dependencies.
Choose the right anchor import based on IDL format:
idl.address exists → new format → import anchor from "@coral-xyz/anchor-new" and new Program(idl, provider)
idl.name at top level → legacy format → import anchor from "@coral-xyz/anchor" and new Program(idl, PROGRAM_ID, provider)
import anchor from "@coral-xyz/anchor-new";
const { Program, AnchorProvider, BN } = anchor;
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("<ADDRESS>");
const USER_WALLET = new PublicKey(process.argv[2]);
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const provider = new AnchorProvider(connection, { publicKey: USER_WALLET }, {});
const idl = await Program.fetchIdl(PROGRAM_ID, provider);
const program = new Program(idl, provider);
const tx = await program.methods
.<instructionName>(<u64/i64 args use new BN(value), pubkey args use new PublicKey(arg)>)
.accounts({
<all accounts mapped out>
})
.transaction();
// If the flow needs a user ATA that may not exist yet, create it first:
// import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from "@solana/spl-token";
// const ata = await getAssociatedTokenAddress(tokenMint, USER_WALLET);
// tx.instructions.unshift(createAssociatedTokenAccountInstruction(USER_WALLET, ata, USER_WALLET, tokenMint));
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = USER_WALLET;
console.log(tx.serialize({ requireAllSignatures: false }).toString("base64"));
- Send the transaction (Phantom simulates automatically and shows a preview first):
send_solana_transaction({
transaction: "<base64 output from script>"
})
The first call (without confirmed: true) runs a simulation and returns expected asset changes. Show the user the preview, then call again with confirmed: true to actually send:
send_solana_transaction({
transaction: "<base64 output from script>",
confirmed: true
})
Adding Payments with CASH
To charge users for skill usage, gate any action behind a CASH payment:
- Call your backend/paywall endpoint to receive a prepared CASH payment transaction (base64).
- Call
pay_api_access({ preparedTx: "<base64>" }).
- Only proceed to the instruction if payment succeeds.
pay_api_access({
preparedTx: "<base64 prepared CASH payment transaction>"
})
---
### Step 6 — Tell the developer what to do next
After writing the file, give a short summary:
Generated skill: //SKILL.md
- tools:
- Phantom MCP: <"detected in session" or "setup instructions included in skill">
Next steps:
- Review the generated accounts in each tool — some may need real addresses (token mints, PDAs).
- Enrich the descriptions if you want clearer agent behavior.
- To charge for skill usage, see the CASH section at the bottom of the skill.
- Share your skill using your platform's distribution flow (git repo, package manager, or direct file copy).
## Rules
- Never fabricate account addresses. Use placeholders like `"<FILL: mSOL mint address>"` for accounts not derivable from the IDL.
- Never reference a skill marketplace, registry, or publishing pipeline that doesn't exist.
- Keep descriptions grounded in what the IDL actually says. If there are no docs, use the instruction name to infer purpose — don't invent behavior.
- System and boilerplate accounts go in the code but not in the "ask the user for" section.