Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/lib/mcp-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,8 @@ pnpm run build && npx -y @modelcontextprotocol/inspector node ./dist/index.js
### 9. Showcase Your Tool with a Demo Agent

Consider showcasing your new MCP tool by building a demo agent in the [templates](https://github.com/EmberAGI/arbitrum-vibekit/tree/main/typescript/templates) directory. Creating a simple agent that uses your tool is a great way to demonstrate its functionality and help others understand how to integrate it into their own projects.

### Existing Tools

- `allora-mcp-server`: Allora network MCP server
- `opensea-mcp-server`: OpenSea marketplace MCP server (read-only tools)
84 changes: 84 additions & 0 deletions typescript/lib/mcp-tools/opensea-actions-mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# OpenSea Actions MCP Server

OpenSea MCP Server for write actions (approvals, listings, purchases) on Arbitrum NFTs.

## Features

- **Arbitrum-only**: All operations restricted to Arbitrum chain
- **Non-custodial**: EIP-712 signing-based execution (no private keys)
- **Environment-gated**: Write operations disabled unless `ENABLE_OPENSEA_WRITE=true`
- **Complete Tool Suite**:
- **Phase 1 (Trading Basics)**:
- `approve_token` - Approve NFT contracts for trading
- `list_nft` - Create fixed-price listings
- `buy_nft` - Purchase listed NFTs (prepare-only by default)
- **Phase 2 (Advanced Operations)**:
- `make_offer` - Create offers for assets or collections
- `cancel_offer` - Cancel existing offers
- `transfer_nft` - Transfer NFTs between wallets
- `wrap_weth` - Wrap ETH to WETH
- `unwrap_weth` - Unwrap WETH to ETH
- `bulk_cancel` - Cancel multiple orders at once

## Installation

```bash
cd typescript/lib/mcp-tools/opensea-actions-mcp-server
pnpm install
pnpm build
```

## Usage

### Environment Variables

```bash
# Required: Arbitrum RPC URL
export ARBITRUM_RPC_URL=https://arbitrum.llamarpc.com

# Optional: Enable write actions (disabled by default)
export ENABLE_OPENSEA_WRITE=true

# Optional: HTTP server port (default: 3050)
export PORT=3050

# Optional: Enable HTTP server
export ENABLE_HTTP=true
```

### Running the Server

#### STDIO Mode (for agents)
```bash
pnpm start
```

#### HTTP/SSE Mode (for MCP Inspector)
```bash
ENABLE_HTTP=true PORT=3050 pnpm start
```

### Testing with MCP Inspector

1. Start the server in HTTP mode
2. Open MCP Inspector
3. Connect to `http://localhost:3050/sse`
4. Test the tools:
- **Phase 1**: `approve_token`, `list_nft`, `buy_nft`
- **Phase 2**: `make_offer`, `cancel_offer`, `transfer_nft`, `wrap_weth`, `unwrap_weth`, `bulk_cancel`

**Example Test Cases:**
- `approve_token` with contract address: `"0x1234567890123456789012345678901234567890"`
- `list_nft` with contract, token_id, price: `contract_address`, `token_id: "123"`, `price: "1.0"`
- `make_offer` for collection offer: `contract_address`, `price: "0.5"`, `offer_type: "collection"`
- `wrap_weth` with amount: `amount: "0.1"`

## Security

- **Non-custodial by default**: All operations prepare transactions but don't execute them
- **Environment gating**: Set `ENABLE_OPENSEA_WRITE=true` to enable actual execution
- **Arbitrum-only**: Operations restricted to Arbitrum chain for security

## Integration

This server is designed to work with the Ember Plugin System for on-chain execution.
49 changes: 49 additions & 0 deletions typescript/lib/mcp-tools/opensea-actions-mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@vibekit/opensea-actions-mcp-server",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"bin": {
"opensea-actions-mcp-server": "./dist/index.js"
},
"scripts": {
"prepare": "pnpm build",
"prepublishOnly": "pnpm build",
"build": "tsc",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
"watch": "nodemon --watch 'src/**/*.ts' --exec 'tsx' src/index.ts"
},
"keywords": [
"mcp",
"opensea",
"nft",
"arbitrum",
"write",
"actions"
],
"author": "",
"license": "MIT",
"description": "OpenSea MCP Server (write actions)",
"dependencies": {
"@modelcontextprotocol/sdk": "catalog:",
"@types/express": "^5.0.1",
"@types/node": "catalog:",
"dotenv": "catalog:",
"express": "catalog:",
"undici": "catalog:",
"p-retry": "^6.2.1",
"nodemon": "^3.1.9",
"typescript": "catalog:",
"zod": "catalog:",
"viem": "catalog:"
},
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"tsx": "catalog:"
}
}
97 changes: 97 additions & 0 deletions typescript/lib/mcp-tools/opensea-actions-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node

import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import dotenv from 'dotenv'
import express from 'express'
import type { NextFunction, Request, Response } from 'express'

import { createServer } from './mcp.js'

dotenv.config()

async function main() {
// Default: HTTP disabled unless explicitly enabled
const httpEnabled = process.env.ENABLE_HTTP === 'true'

const rpcUrl = process.env.ARBITRUM_RPC_URL || 'https://arbitrum.llamarpc.com'

const server = await createServer({ rpcUrl })

if (httpEnabled) {
const app = express()

// Permissive CORS for Inspector UI and other local tools
app.use(function (req: Request, res: Response, next: NextFunction) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
res.sendStatus(204)
return
}
next()
})

app.use(function (req: Request, _res: Response, next: NextFunction) {
console.error(`${req.method} ${req.url}`)
next()
})

const transports: { [sessionId: string]: SSEServerTransport } = {}

app.get('/sse', async (_req: Request, res: Response) => {
console.error('Received connection')

const transport = new SSEServerTransport('/messages', res)
transports[transport.sessionId] = transport

await server.connect(transport)
})

app.post('/messages', async (_req: Request, res: Response) => {
const sessionId = _req.query.sessionId as string
console.error(`Received message for session: ${sessionId}`)

let bodyBuffer = Buffer.alloc(0)

_req.on('data', (chunk: Buffer) => {
bodyBuffer = Buffer.concat([bodyBuffer, chunk])
})

_req.on('end', async () => {
try {
const bodyStr = bodyBuffer.toString('utf8')
const bodyObj = JSON.parse(bodyStr)
console.log(`${JSON.stringify(bodyObj, null, 4)}`)
} catch (error) {
console.error(`Error handling request: ${error}`)
}
})
const transport = transports[sessionId]
if (!transport) {
res.status(400).send('No transport found for sessionId')
return
}
await transport.handlePostMessage(_req, res)
})

const PORT = process.env.PORT || 3050
app.listen(PORT, () => {
console.error(`OpenSea Actions MCP server is running on port ${PORT}`)
})
}

const stdioTransport = new StdioServerTransport()
console.error('Initializing stdio transport...')
await server.connect(stdioTransport)
console.error('OpenSea Actions MCP stdio server started and connected.')
console.error('Server is now ready to receive stdio requests.')

process.stdin.on('end', () => {
console.error('Stdio connection closed, exiting...')
process.exit(0)
})
}

main().catch(() => process.exit(-1))
Loading