A local read-only MCP server for Etsy shops, active listings, and listing variation inventory.
Runs on your machine from its package; the steps are below.
At most: only reads or looks up information.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted โ your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
npx -y @profplum700/etsy-mcp-server
MCP client config
{
"mcpServers": {
"etsy": {
"command": "npx",
"args": [
"-y",
"@profplum700/etsy-mcp-server"
]
}
}
}A modern, universal TypeScript/JavaScript client for the Etsy Open API v3 with full OAuth 2.0 PKCE authentication support. Works seamlessly in both browser and Node.js environments.
This repository is heavily based on the official Etsy OpenAPI specification (https://www.etsy.com/openapi/generated/oas/3.0.0.json), as referenced in Etsy's API documentation (https://developers.etsy.com/documentation/reference).
npm install @profplum700/etsy-v3-api-client
yarn add @profplum700/etsy-v3-api-client
pnpm add @profplum700/etsy-v3-api-client
The companion @profplum700/etsy-mcp-server package runs locally over stdio and exposes read-only shop, active-listing, and listing-inventory tools. It requires Node.js 24+, your own approved Etsy developer app, and an OS credential store. It does not use a hosted endpoint.
Register http://localhost:3030/oauth/redirect as the Etsy app callback if required, then run this in a local interactive terminal:
npx --yes @profplum700/etsy-mcp-server@latest setup
Enter your keystring and shared secret in the hidden terminal prompts, authorize only shops_r and listings_r in Etsy, then choose whether to add the server to your user-level Codex MCP configuration. For another stdio client, configure npx --yes @profplum700/etsy-mcp-server@latest serve as its launch command.
You can connect multiple shops without replacing earlier profiles. Run setup --reuse-app --manual-browser to reuse the saved developer app credentials, then open the printed authorization URL in the browser profile signed in to the shop you want to add. Use status to see saved shops and use <shop name or ID> to choose which one MCP tools query. disconnect removes only the active shop; disconnect --all removes every saved profile. Use remove-codex to remove the Codex entry without disconnecting any shop.
Prompt for an agent: โSet up the local Etsy MCP server. Do not ask me to paste Etsy credentials into chat or save them in a repository or environment file. Run the setup command in an interactive local terminal so I can enter them there; request only shops_r and listings_r, verify my shop, and offer the user-level Codex connection.โ
import { EtsyClient, AuthHelper } from '@profplum700/etsy-v3-api-client';
// Create authentication helper
const authHelper = new AuthHelper({
keystring: 'your-api-key',
redirectUri: 'https://your-app.com/callback',
scopes: ['shops_r', 'listings_r']
});
// Get authorization URL
const authUrl = await authHelper.getAuthUrl();
console.log('Visit this URL to authorize:', authUrl);
// After user authorization, exchange code for tokens
const state = await authHelper.getState();
await authHelper.setAuthorizationCode('authorization-code-from-callback', state);
const tokens = await authHelper.getAccessToken();
// Create API client
const client = new EtsyClient({
keystring: 'your-api-key',
sharedSecret: 'your-shared-secret', // REQUIRED for v3 API compliance
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: tokens.expires_at
});
// Make API calls
const user = await client.getUser();
console.log('User:', user);
<script type="module">
import { EtsyClient, AuthHelper } from 'https://unpkg.com/@profplum700/etsy-v3-api-client/dist/browser.esm.js';
// Your code here...
</script>
Or using UMD:
<script src="https://unpkg.com/@profplum700/etsy-v3-api-client/dist/browser.umd.js"></script>
<script>
const { EtsyClient, AuthHelper } = EtsyApiClient;
// Your code here...
</script>
// CommonJS
const { EtsyClient, AuthHelper } = require('@profplum700/etsy-v3-api-client');
// ES Modules
import { EtsyClient, AuthHelper } from '@profplum700/etsy-v3-api-client';
import { AuthHelper, ETSY_SCOPES } from '@profplum700/etsy-v3-api-client';
const authHelper = new AuthHelper({
keystring: 'your-api-key',
redirectUri: 'https://your-app.com/callback',
scopes: [
ETSY_SCOPES.SHOPS_READ,
ETSY_SCOPES.LISTINGS_READ,
ETSY_SCOPES.PROFILE_READ
]
});
// Step 1: Get authorization URL
const authUrl = await authHelper.getAuthUrl();
// Redirect user to authUrl
// Step 2: Handle callback (in your callback endpoint)
const { code, state } = getCallbackParams(); // Your implementation
const expectedState = await authHelper.getState();
if (state === expectedState) {
await authHelper.setAuthorizationCode(code, state);
const tokens = await authHelper.getAccessToken();
// Store tokens securely
}
import { ETSY_SCOPES, COMMON_SCOPE_COMBINATIONS } from '@profplum700/etsy-v3-api-client';
// Individual scopes
const scopes = [
ETSY_SCOPES.SHOPS_READ,
ETSY_SCOPES.LISTINGS_WRITE,
ETSY_SCOPES.TRANSACTIONS_READ
];
// Pre-defined combinations
const readOnlyScopes = COMMON_SCOPE_COMBINATIONS.SHOP_READ_ONLY;
const managementScopes = COMMON_SCOPE_COMBINATIONS.SHOP_MANAGEMENT;
import { EtsyClient } from '@profplum700/etsy-v3-api-client';
const client = new EtsyClient({
keystring: 'your-api-key',
sharedSecret: 'your-shared-secret', // Get this from Your Apps page on Etsy
accessToken: 'user-access-token',
refreshToken: 'user-refresh-token',
expiresAt: new Date('2024-12-31T23:59:59Z'),
// Optional configuration
rateLimiting: {
enabled: true,
maxRequestsPerSecond: 5,
maxRequestsPerDay: 5000 // Conservative local fallback; Etsy response headers report this app's actual quota
},
caching: {
enabled: true,
ttl: 300 // 5 minutes
}
});
// Get current user
const user = await client.getUser();
// Get user's shops
const shops = await client.getUserShops();
// Get shop listings
const listings = await client.getListingsByShop('shop-id');
// Get specific listing
const listing = await client.getListing('listing-id');
// Search listings
const searchResults = await client.findAllListingsActive({
keywords: 'vintage',
taxonomy_id: 123,
limit: 25
});
import { EtsyApiError, EtsyAuthError, EtsyRateLimitError } from '@profplum700/etsy-v3-api-client';
try {
const user = await client.getUser();
} catch (error) {
if (error instanceof EtsyAuthError) {
// Handle authentication errors
console.error('Auth error:', error.message, error.code);
} else if (error instanceof EtsyRateLimitError) {
// Handle rate limiting
console.error('Rate limited. Retry after:', error.retryAfter);
} else if (error instanceof EtsyApiError) {
// Handle API errors
console.error('API error:', error.statusCode, error.message);
} else {
// Handle other errors
console.error('Unexpected error:', error);
}
}
The client provides several storage mechanisms:
import {
createDefaultTokenStorage,
LocalStorageTokenStorage,
SessionStorageTokenStorage,
FileTokenStorage,
MemoryTokenStorage
} from '@profplum700/etsy-v3-api-client';
// Automatic storage selection based on environment
const storage = createDefaultTokenStorage();
// Browser localStorage
const localStorage = new LocalStorageTokenStorage('etsy-tokens');
// Browser sessionStorage
const sessionStorage = new SessionStorageTokenStorage('etsy-tokens');
// Node.js file storage
const fileStorage = new FileTokenStorage('./tokens.json');
// In-memory storage (not persistent)
const memoryStorage = new MemoryTokenStorage();
// Use with client
const client = new EtsyClient(config, storage);
import { TokenStorage } from '@profplum700/etsy-v3-api-client';
class CustomTokenStorage implements TokenStorage {
async save(tokens: EtsyTokens): Promise<void> {
// Your save implementation
}
async load(): Promise<EtsyTokens | null> {
// Your load implementation
}
async clear(): Promise<void> {
// Your clear implementation
}
}
The client includes built-in rate limiting to respect Etsy's API limits:
const client = new EtsyClient({
// ... other config
rateLimiting: {
enabled: true,
maxRequestsPerSecond: 5, // Conservative QPS fallback
maxRequestsPerDay: 5000, // Conservative rolling 24-hour fallback
minRequestInterval: 200 // Minimum ms between requests (5 QPS fallback)
}
});
// Check remaining requests in the rolling local window
const remaining = client.getRemainingRequests();
console.log(`${remaining} requests remaining in the rolling 24-hour window`);
When building a custom transport around the exported EtsyRateLimiter, keep the reservation ID attached to its response so concurrent, out-of-order headers cannot incorrectly restore exhausted quota:
const reservationId = await rateLimiter.waitForRateLimitWithReservation();
let response: Response;
try {
response = await fetch(url);
} catch (error) {
rateLimiter.releaseRequestSlot(reservationId);
throw error;
}
rateLimiter.updateFromHeaders(response.headers, reservationId);
waitForRateLimit() remains available for source compatibility when you only need dispatch pacing. For custom transports that also feed response headers back to the limiter, use the reservation-aware method above; uncorrelated positive headers cannot safely reopen exhausted quota.
Optional response caching to improve performance:
const client = new EtsyClient({
// ... other config
caching: {
enabled: true,
ttl: 300 // Cache for 5 minutes
}
});
// Clear cache when needed
await client.clearCache();
The package provides optimized builds for different environments:
{
"imports": {
"@profplum700/etsy-v3-api-client": "./dist/index.esm.js",
"@profplum700/etsy-v3-api-client/browser": "./dist/browser.esm.js",
"@profplum700/etsy-v3-api-client/node": "./dist/node.esm.js"
}
}
getUser() - Get current user infogetUserShops(userId?) - Get user's shopsgetShop(shopId) - Get shop detailsgetShopByOwnerUserId(userId) - Get shop by owner user IDgetListingPersonalizations(listingId) - Get personalization questions for a listingupdateListingPersonalization(shopId, listingId, params) - Create or update personalization questionsdeleteListingPersonalization(shopId, listingId) - Delete personalizationgetListing(listingId) - Get listing detailsgetListingsByShop(shopId, options?) - Get shop's listingsfindAllListingsActive(options?) - Search active listings| Category | Endpoint | Supported |
|---|---|---|
| BuyerTaxonomy | getBuyerTaxonomyNodes | Yes |
| BuyerTaxonomy | getPropertiesByBuyerTaxonomyId | No |
| SellerTaxonomy | getSellerTaxonomyNodes | Yes |
| SellerTaxonomy | getPropertiesByTaxonomyId | Yes |
| ShopListing | createDraftListing | Yes |
| ShopListing | getListingsByShop | Yes |
| ShopListing | deleteListing | Yes |
| ShopListing | getListing | Yes |
| ShopListing | findAllListingsActive | Yes |
| ShopListing | findAllActiveListingsByShop | No |
| ShopListing | getListingsByListingIds | No |
| ShopListing | getFeaturedListingsByShop | No |
| ShopListing | deleteListingProperty | No |
| ShopListing | updateListingProperty | Yes |
| ShopListing | getListingProperty | No |
| ShopListing | getListingProperties | Yes |
| ShopListing | updateListing | Yes |
| ShopListing | getListingsByShopReceipt | No |
| ShopListing | getListingsByShopReturnPolicy | No |
| ShopListing | getListingsByShopSectionId | No |
| ShopListing | getListingPersonalization | Yes |
| ShopListing | updateListingPersonalization | Yes |
| ShopListing | deleteListingPersonalization | Yes |
| ShopListing File | deleteListingFile | No |
| ShopListing File | getListingFile | No |
| ShopListing File | getAllListingFiles | No |
| ShopListing File | uploadListingFile | No |
| ShopListing Image | deleteListingImage | Yes |
| ShopListing Image | getListingImage | Yes |
| ShopListing Image | getListingImages | Yes |
| ShopListing Image | uploadListingImage | Yes |
| ShopListing Inventory | getListingInventory | Yes |
| ShopListing Inventory | updateListingInventory | Yes |
| ShopListing Offering | getListingOffering | No |
| ShopListing Product | getListingProduct | No |
| ShopListing Translation | createListingTranslation | No |
| ShopListing Translation | getListingTranslation | No |
| ShopListing Translation | updateListingTranslation | No |
| ShopListing VariationImage | getListingVariationImages | No |
| ShopListing VariationImage | updateVariationImages | No |
| ShopListing Video | deleteListingVideo | No |
| ShopListing Video | getListingVideo | No |
| ShopListing Video | getListingVideos | No |
| ShopListing Video | uploadListingVideo | No |
| Other | ping | No |
| Other | tokenScopes | No |
| Ledger Entry | getShopPaymentAccountLedgerEntry | Yes |
| Ledger Entry | getShopPaymentAccountLedgerEntries | Yes |
| Payment | getShopPaymentByReceiptId | No |
| Payment | getPaymentAccountLedgerEntry | No |
| Payment | getPayments | No |
| Shop Receipt | getShopReceipt | Yes |
| Shop Receipt | updateShopReceipt | Yes |
| Shop Receipt | getShopReceipts | Yes |
| Shop Receipt | searchAllShopReceipts | No |
| Shop Receipt Transactions | getShopReceiptTransaction | Yes |
| Shop Receipt Transactions | getShopReceiptTransactionsByListing | No |
| Shop Receipt Transactions | getShopReceiptTransactionsByReceipt | Yes |
| Shop Receipt Transactions | getShopReceiptTransactionsByShop | No |
| Review | getReviewsByListing | Yes |
| Review | getReviewsByShop | Yes |
| Shop HolidayPreferences | getShopHolidayPreferences | No |
| Shop HolidayPreferences | updateShopHolidayPreferences | No |
| Shop ProcessingProfiles | createShopShippingProfileUpgrade | No |
| Shop ProcessingProfiles | getShopShippingProfileUpgrades | Yes |
| Shop ProcessingProfiles | deleteShopShippingProfileUpgrade | No |
| Shop ProcessingProfiles | getShopShippingProfileUpgrade | No |
| Shop ProcessingProfiles | updateShopShippingProfileUpgrade | No |
| Shop ShippingProfile | getShippingCarriers | No |
| Shop ShippingProfile | createShopShippingProfile | Yes |
| Shop ShippingProfile | getShopShippingProfiles | Yes |
| Shop ShippingProfile | deleteShopShippingProfile | Yes |
| Shop ShippingProfile | getShopShippingProfile | Yes |
| Shop ShippingProfile | updateShopShippingProfile | Yes |
| Shop ShippingProfile | createShopShippingProfileDestination | Yes |
| Shop ShippingProfile | getShopShippingProfileDestinationsByShippingProfile | Yes |
| Shop ShippingProfile | deleteShopShippingProfileDestination | Yes |
| Shop ShippingProfile | updateShopShippingProfileDestination | Yes |
| Shop ShippingProfile | createShopShippingProfileUpgrade | No |
| Shop ShippingProfile | getShopShippingProfileUpgrades | Yes |
| Shop ShippingProfile | deleteShopShippingProfileUpgrade | No |
| Shop ShippingProfile | updateShopShippingProfileUpgrade | No |
| Shop | getShop | Yes |
| Shop | updateShop | Yes |
| Shop | getShopByOwnerUserId | Yes |
| Shop | findShops | No |
| Shop ProductionPartner | getShopProductionPartners | Yes |
| Shop Section | createShopSection | Yes |
| Shop Section | getShopSections | Yes |
| Shop Section | deleteShopSection | Yes |
| Shop Section | getShopSection | Yes |
| Shop Section | updateShopSection | Yes |
| Shop Return Policy | consolidateShopReturnPolicies | No |
| Shop Return Policy | createShopReturnPolicy | No |
| Shop Return Policy | getShopReturnPolicies | No |
| Shop Return Policy | deleteShopReturnPolicy | No |
| Shop Return Policy | getShopReturnPolicy | No |
| Shop Return Policy | updateShopReturnPolicy | No |
| User | getUser | Yes |
| User | getMe | No |
| UserAddress | deleteUserAddress | No |
| UserAddress | getUserAddress | No |
| UserAddress | getUserAddresses | No |
getReviewsByListing(listingId, options?) - Get reviews for a listinggetReviewsByShop(shopId, options?) - Get reviews for a shopfindAllListingsActive(params) - Search listings with filtersgetAuthUrl() - Generate authorization URLsetAuthorizationCode(code, state) - Set auth code from callbackgetAccessToken() - Exchange code for tokensgetState() - Get current state parametergetCodeVerifier() - Get PKCE code verifiergetAccessToken() - Get current access token (refreshes if needed)refreshToken() - Manually refresh tokensgetCurrentTokens() - Get current token setisTokenExpired() - Check if token is expiredclearTokens() - Clear stored tokensinterface EtsyClientConfig {
keystring: string; // Required: Your API key
sharedSecret: string; // Required: Your shared secret (from Your Apps page)
accessToken?: string; // User's access token
refreshToken?: string; // User's refresh token
expiresAt?: Date; // Token expiration date
refreshSave?: (token, refresh, expires) => void; // Token save callback
rateLimiting?: {
enabled?: boolean; // Enable rate limiting
maxRequestsPerSecond?: number; // Requests per second limit
maxRequestsPerDay?: number; // Daily request limit
minRequestInterval?: number; // Minimum interval between requests (ms)
};
caching?: {
enabled?: boolean; // Enable response caching
ttl?: number; // Cache TTL in seconds
};
}
import { AuthHelper, EtsyClient, createDefaultTokenStorage } from '@profplum700/etsy-v3-api-client';
async function authenticateAndFetchData() {
// Step 1: Initialize authentication
const authHelper = new AuthHelper({
keystring: process.env.ETSY_API_KEY!,
redirectUri: 'http://localhost:3000/callback',
scopes: ['shops_r', 'listings_r', 'profile_r']
});
// Step 2: Get authorization URL
const authUrl = await authHelper.getAuthUrl();
console.log('Visit:', authUrl);
// Step 3: Handle callback (pseudo-code)
const { code, state } = await waitForCallback();
const expectedState = await authHelper.getState();
if (state !== expectedState) {
throw new Error('State mismatch');
}
// Step 4: Exchange for tokens
await authHelper.setAuthorizationCode(code, state);
const tokens = await authHelper.getAccessToken();
// Step 5: Create client with storage
const storage = createDefaultTokenStorage();
const client = new EtsyClient({
keystring: process.env.ETSY_API_KEY!,
...tokens
}, storage);
// Step 6: Use the API
const user = await client.getUser();
const shops = await client.getUserShops();
if (shops.length > 0) {
const listings = await client.getListingsByShop(shops[0].shop_id.toString());
console.log(`Found ${listings.length} listings`);
}
}
Here's a complete working example for browser environments:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Etsy API Client - Browser Example</title>
</head>
<body>
<div id="app">
<h1>Etsy API Browser Example</h1>
<button id="loginBtn">Login with Etsy</button>
<div id="userInfo" style="display: none;">
<h2>User Information</h2>
<div id="userData"></div>
<button id="getShopsBtn">Get My Shops</button>
<div id="shopsData"></div>
</div>
</div>
<script type="module">
import { AuthHelper, EtsyClient, ETSY_SCOPES } from 'https://unpkg.com/@profplum700/etsy-v3-api-client@1.0.0/dist/browser.esm.js';
// Replace with your actual API key
const API_KEY = 'your-api-key-here';
const REDIRECT_URI = window.location.origin + window.location.pathname;
let authHelper;
let client;
// Initialize auth helper
function initAuth() {
authHelper = new AuthHelper({
keystring: API_KEY,
redirectUri: REDIRECT_URI,
scopes: [
ETSY_SCOPES.SHOPS_READ,
ETSY_SCOPES.LISTINGS_READ,
ETSY_SCOPES.PROFILE_READ
]
});
}
// Handle login button click
document.getElementById('loginBtn').addEventListener('click', async () => {
const authUrl = await authHelper.getAuthUrl();
window.location.href = authUrl;
});
// Handle OAuth callback
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code && state) {
try {
const expectedState = await authHelper.getState();
if (state === expectedState) {
await authHelper.setAuthorizationCode(code, state);
const tokens = await authHelper.getAccessToken();
// Create client
client = new EtsyClient({
keystring: API_KEY,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: tokens.expires_at,
rateLimiting: { enabled: true },
caching: { enabled: true, ttl: 300 }
});
// Store tokens in localStorage for persistence
localStorage.setItem('etsy_tokens', JSON.stringify(tokens));
// Clear URL parameters
window.history.replaceState({}, document.title, window.location.pathname);
// Show user info
await showUserInfo();
} else {
console.error('State mismatch');
}
} catch (error) {
console.error('Authentication failed:', error);
alert('Authentication failed. Please try again.');
}
}
}
// Show user information
async function showUserInfo() {
try {
const user = await client.getUser();
document.getElementById('loginBtn').style.display = 'none';
document.getElementById('userInfo').style.display = 'block';
document.getElementById('userData').innerHTML = `
<p><strong>User ID:</strong> ${user.user_id}</p>
<p><strong>Login Name:</strong> ${user.login_name || 'N/A'}</p>
<p><strong>Primary Email:</strong> ${user.primary_email || 'N/A'}</p>
`;
} catch (error) {
console.error('Failed to get user info:', error);
alert('Failed to get user information.');
}
}
// Get user shops
document.getElementById('getShopsBtn').addEventListener('click', async () => {
try {
const shops = await client.getUserShops();
if (shops.length > 0) {
const shopsHtml = shops.map(shop => `
<div style="border: 1px solid #ccc; padding: 10px; margin: 10px 0;">
<h3>${shop.shop_name}</h3>
<p><strong>Shop ID:</strong> ${shop.shop_id}</p>
<p><strong>Title:</strong> ${shop.title || 'N/A'}</p>
<p><strong>URL:</strong> <a href="${shop.url}" target="_blank">${shop.url}</a></p>
</div>
`).join('');
document.getElementById('shopsData').innerHTML = `
<h3>Your Shops (${shops.length})</h3>
${shopsHtml}
`;
} else {
document.getElementById('shopsData').innerHTML = '<p>No shops found.</p>';
}
} catch (error) {
console.error('Failed to get shops:', error);
alert('Failed to get shop information.');
}
});
// Check for existing tokens on page load
async function checkExistingAuth() {
const storedTokens = localStorage.getItem('etsy_tokens');
if (storedTokens) {
try {
const tokens = JSON.parse(storedTokens);
// Check if tokens are still valid
if (new Date(tokens.expires_at) > new Date()) {
client = new EtsyClient({
keystring: API_KEY,
...tokens,
rateLimiting: { enabled: true },
caching: { enabled: true, ttl: 300 }
});
await showUserInfo();
} else {
// Tokens expired, clear them
localStorage.removeItem('etsy_tokens');
}
} catch (error) {
console.error('Failed to restore session:', error);
localStorage.removeItem('etsy_tokens');
}
}
}
// Initialize the application
initAuth();
await handleCallback();
await checkExistingAuth();
</script>
</body>
</html>
This example demonstrates:
import { useState, useEffect } from 'react';
import { EtsyClient, createDefaultTokenStorage } from '@profplum700/etsy-v3-api-client';
function useEtsyClient(tokens) {
const [client, setClient] = useState(null);
useEffect(() => {
if (tokens) {
const storage = createDefaultTokenStorage({ preferSession: true });
const etsyClient = new EtsyClient({
keystring: process.env.REACT_APP_ETSY_API_KEY,
...tokens,
rateLimiting: { enabled: true },
caching: { enabled: true, ttl: 300 }
}, storage);
setClient(etsyClient);
}
}, [tokens]);
return client;
}
Comprehensive documentation and examples are available in the docs/ directory:
Complete, working example applications demonstrating various use cases:
Contributions are welcome! Please read our Contributing Guidelines for details.
git checkout -b feature/amazing-featuregit commit -m 'Add amazing feature'git push origin feature/amazing-featureThis project is licensed under the MIT License - see the LICENSE file for details.
If you have questions or need help:
See CHANGELOG.md for version history and release notes.