From miro-pack
Install and configure Miro REST API v2 authentication with OAuth 2.0. Use when setting up a new Miro app, configuring OAuth tokens, or initializing the @mirohq/miro-api Node.js client. Trigger with phrases like "install miro", "setup miro", "miro auth", "miro OAuth", "configure miro API".
npx claudepluginhub flight505/skill-forge --plugin miro-packThis skill is limited to using the following tools:
Set up the official `@mirohq/miro-api` Node.js client and configure OAuth 2.0 authentication against the Miro REST API v2 (`https://api.miro.com/v2/`).
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Set up the official @mirohq/miro-api Node.js client and configure OAuth 2.0 authentication against the Miro REST API v2 (https://api.miro.com/v2/).
# Official Miro Node.js client
npm install @mirohq/miro-api
# For Express-based OAuth callback server
npm install express dotenv
# .env (NEVER commit — add to .gitignore)
MIRO_CLIENT_ID=your_client_id
MIRO_CLIENT_SECRET=your_client_secret
MIRO_REDIRECT_URI=http://localhost:3000/auth/miro/callback
MIRO_ACCESS_TOKEN= # Filled after OAuth flow
MIRO_REFRESH_TOKEN= # Filled after OAuth flow
Miro uses standard OAuth 2.0 authorization code flow. Tokens expire in 3599 seconds (approximately 1 hour). Always store and use the refresh token.
// src/auth.ts
import { Miro } from '@mirohq/miro-api';
import express from 'express';
// High-level client handles token management
const miro = new Miro({
clientId: process.env.MIRO_CLIENT_ID!,
clientSecret: process.env.MIRO_CLIENT_SECRET!,
redirectUrl: process.env.MIRO_REDIRECT_URI!,
// Storage adapter for tokens (implement for production)
storage: {
async get(userId: string) {
// Return stored token for user
return getTokenFromDB(userId);
},
async set(userId: string, token) {
// Persist token
await saveTokenToDB(userId, token);
},
},
});
const app = express();
// Step 1: Redirect user to Miro authorization page
app.get('/auth/miro', (req, res) => {
const authUrl = miro.getAuthUrl();
res.redirect(authUrl);
});
// Step 2: Handle OAuth callback
app.get('/auth/miro/callback', async (req, res) => {
const { code } = req.query;
if (!code || typeof code !== 'string') {
return res.status(400).send('Missing authorization code');
}
try {
// Exchange code for access_token + refresh_token
await miro.exchangeCodeForAccessToken('default-user', code);
res.send('Miro connected successfully!');
} catch (err) {
console.error('Token exchange failed:', err);
res.status(500).send('Authentication failed');
}
});
app.listen(3000, () => console.log('OAuth server at http://localhost:3000'));
For scripts and automation where you already have an access token:
// src/client.ts
import { MiroApi } from '@mirohq/miro-api';
// Low-level stateless client — pass token directly
const api = new MiroApi(process.env.MIRO_ACCESS_TOKEN!);
// Verify connection by listing boards
async function verifyConnection() {
const boards = await api.getBoards();
console.log(`Connected! Found ${boards.body.data?.length ?? 0} boards`);
return true;
}
verifyConnection().catch(console.error);
In your Miro app settings (https://developers.miro.com), enable the scopes your app requires:
| Scope | Purpose | Required For |
|---|---|---|
boards:read | Read board data, items, members | GET endpoints |
boards:write | Create/update/delete boards and items | POST/PUT/PATCH/DELETE endpoints |
team:read | Read team info and members | Team management |
team:write | Manage team membership | Team provisioning |
organizations:read | Read org structure | Enterprise features |
identity:read | Read user profile | User identification |
auditlogs:read | Read audit logs | Enterprise compliance |
Token response after successful exchange:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"expires_in": 3599,
"scope": "boards:read boards:write",
"user_id": "1234567890",
"team_id": "9876543210"
}
| Error | HTTP Status | Cause | Solution |
|---|---|---|---|
insufficientPermissions | 403 | Missing OAuth scope | Add required scope in app settings and re-authorize |
tokenExpired | 401 | Access token expired | Use refresh token to get new access token |
invalidGrant | 400 | Auth code already used or expired | Restart OAuth flow from the beginning |
invalidClient | 401 | Wrong client_id or client_secret | Verify credentials in Miro app settings |
ENOTFOUND api.miro.com | N/A | DNS/network failure | Check internet and firewall rules |
async function refreshAccessToken(): Promise<string> {
const response = await fetch('https://api.miro.com/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.MIRO_CLIENT_ID!,
client_secret: process.env.MIRO_CLIENT_SECRET!,
refresh_token: process.env.MIRO_REFRESH_TOKEN!,
}),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const data = await response.json();
// Store new tokens
process.env.MIRO_ACCESS_TOKEN = data.access_token;
process.env.MIRO_REFRESH_TOKEN = data.refresh_token;
return data.access_token;
}
After successful auth, proceed to miro-hello-world for your first board and item operations.