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)
48 changes: 48 additions & 0 deletions typescript/lib/mcp-tools/opensea-mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# OpenSea MCP Server (Read-only)

MCP server exposing read-only OpenSea marketplace endpoints, optimized for Arbitrum by default.

## Features

- get_wallet_nfts: Fetch NFTs owned by wallet on a chain (default arbitrum)
- get_collection: Fetch collection metadata by slug
- search_collections: Search collections
- get_listings_by_collection: Fetch active listings for a collection on a chain

## Setup

1. Add env:

```bash
OPENSEA_API_KEY=your_key_here
# Optional
OPENSEA_BASE_URL=https://api.opensea.io
PORT=3011
```

2. Build and run:

```bash
pnpm -w build
pnpm -F @vibekit/opensea-mcp-server dev
```

3. Inspect with MCP Inspector:

```bash
npx -y @modelcontextprotocol/inspector node ./dist/index.js
```

## Tool Schemas

- get_wallet_nfts({ address, chain='arbitrum', cursor? })
- get_collection({ collection_slug })
- search_collections({ q, chain?, page?, limit? })
- get_listings_by_collection({ collection_slug, chain='arbitrum', limit? })

## Notes

- Requires OPENSEA_API_KEY
- Read-only only. For write flows (list/buy), integrate wallet signing via an agent skill, not this server.


48 changes: 48 additions & 0 deletions typescript/lib/mcp-tools/opensea-mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@vibekit/opensea-mcp-server",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"bin": {
"opensea-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"
],
"author": "",
"license": "MIT",
"description": "OpenSea MCP Server (read-only)",
"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:"
},
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"tsx": "catalog:"
}
}


33 changes: 33 additions & 0 deletions typescript/lib/mcp-tools/opensea-mcp-server/scripts/sse-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'

async function main() {
const sseUrl = process.env.SSE_URL || 'http://localhost:3035/sse'

const client = new Client(
{ name: 'opensea-e2e-client', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
)

const transport = new SSEClientTransport(new URL(sseUrl))
await client.connect(transport)
console.log(`[client] connected to ${sseUrl}`)

const tools = await client.listTools()
console.log('[client] tools:', JSON.stringify(tools, null, 2))

const result = await client.callTool({
name: 'get_collection',
arguments: { collection_slug: 'alchemy-denver2025-blue' },
})
console.log('[client] get_collection result:', JSON.stringify(result, null, 2))

await client.close()
}

main().catch((err) => {
console.error('[client] error:', err)
process.exit(1)
})


101 changes: 101 additions & 0 deletions typescript/lib/mcp-tools/opensea-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/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'

let apiKey = process.env.OPENSEA_API_KEY || ''
if (!apiKey) {
console.error('Warning: OPENSEA_API_KEY not set; OpenSea calls will 401 until provided')
}

const server = await createServer({ apiKey })
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 || 3011
app.listen(PORT, () => {
console.error(`OpenSea MCP server is running on port ${PORT}`)
})
}

const stdioTransport = new StdioServerTransport()
console.error('Initializing stdio transport...')
await server.connect(stdioTransport)
console.error('OpenSea 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