ebay-mcp 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +11 -0
- package/build/scripts/setup.js +18 -0
- package/build/scripts/skills.js +238 -0
- package/build/skills/content.js +141 -0
- package/build/skills/index.js +54 -0
- package/build/skills/install.js +75 -0
- package/build/skills/markers.js +25 -0
- package/build/skills/registry-snapshot.js +38 -0
- package/build/skills/render.js +75 -0
- package/build/skills/targets.js +93 -0
- package/build/skills/types.js +12 -0
- package/build/tools/categories/index.js +23 -16
- package/build/utils/cli-ui.js +47 -0
- package/package.json +3 -1
package/build/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
3
3
|
import { validateEnvironmentConfig } from './config/environment.js';
|
|
4
4
|
import { createEbayMcpRuntime } from './mcp/runtime.js';
|
|
5
5
|
import { runSetup } from './scripts/setup.js';
|
|
6
|
+
import { runSkillsWizard } from './scripts/skills.js';
|
|
6
7
|
import { getErrorMessage } from './utils/errors.js';
|
|
7
8
|
import { serverLogger, getLogPaths } from './utils/logger.js';
|
|
8
9
|
import { getCachedUpdateNotice } from './utils/version.js';
|
|
@@ -17,6 +18,16 @@ if (args.includes('setup')) {
|
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
if (args.includes('skills')) {
|
|
22
|
+
try {
|
|
23
|
+
await runSkillsWizard();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error('Skills install failed:', error instanceof Error ? error.message : error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
20
31
|
/**
|
|
21
32
|
* eBay API MCP Server
|
|
22
33
|
* Provides access to eBay APIs through Model Context Protocol
|
package/build/scripts/setup.js
CHANGED
|
@@ -12,6 +12,8 @@ import { getOAuthAuthorizationUrl } from '../config/environment.js';
|
|
|
12
12
|
import { startCallbackServer } from '../utils/oauth-helper.js';
|
|
13
13
|
import { defineWizard, runWizard, ClackRenderer } from '../utils/setup-wizard.js';
|
|
14
14
|
import { loadExistingConfig, quoteEnvValue } from './setup-shared.js';
|
|
15
|
+
import { runSkillsWizard } from './skills.js';
|
|
16
|
+
import prompts from 'prompts';
|
|
15
17
|
import { configureLLMClient, detectLLMClients } from '../utils/llm-client-detector.js';
|
|
16
18
|
import { runSecurityChecks, displaySecurityResults } from '../utils/security-checker.js';
|
|
17
19
|
import { validateSetup, displayRecommendations } from '../utils/setup-validator.js';
|
|
@@ -906,6 +908,22 @@ export async function runSetup() {
|
|
|
906
908
|
console.log(' 3. Try: "Show my eBay seller information"\n');
|
|
907
909
|
console.log(ui.dim(' Documentation: ') + ui.info('https://github.com/YosefHayim/ebay-mcp'));
|
|
908
910
|
console.log(ui.dim(' Get Help: ') + ui.info('https://github.com/YosefHayim/ebay-mcp/issues\n'));
|
|
911
|
+
// Optional: offer to install agent skills (Codex / Claude Code / Cursor) so the
|
|
912
|
+
// user's assistant knows how to drive the tools. Skipped in non-interactive runs.
|
|
913
|
+
if (process.stdin.isTTY) {
|
|
914
|
+
const { wantSkills } = (await prompts({
|
|
915
|
+
type: 'confirm',
|
|
916
|
+
name: 'wantSkills',
|
|
917
|
+
message: 'Install eBay AI skills (Codex / Claude Code / Cursor) so your assistant drives the tools faster?',
|
|
918
|
+
initial: true,
|
|
919
|
+
}));
|
|
920
|
+
if (wantSkills) {
|
|
921
|
+
await runSkillsWizard({ fromSetup: true });
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
showInfo('You can install them later with: npm run skills');
|
|
925
|
+
}
|
|
926
|
+
}
|
|
909
927
|
}
|
|
910
928
|
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
911
929
|
function parseArgs() {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import { ALL_PROVIDERS, applyWrite, buildRegistrySnapshot, detectProvider, planWrite, providerLabel, renderSkillTargets, } from '../skills/index.js';
|
|
6
|
+
import { ebayPalette, printHeading, printInfo, printRule, printSuccess, printWarning, ui, } from '../utils/cli-ui.js';
|
|
7
|
+
const ALL_LAYERS = ['using', 'contributing'];
|
|
8
|
+
/** Splits a comma list flag value into trimmed, non-empty tokens. */
|
|
9
|
+
function parseList(value) {
|
|
10
|
+
return (value ?? '')
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((token) => token.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
/** Reads the value after a `--flag value` or `--flag=value` argument. */
|
|
16
|
+
function readFlagValue(argv, flag) {
|
|
17
|
+
const inline = argv.find((arg) => arg.startsWith(`${flag}=`));
|
|
18
|
+
if (inline)
|
|
19
|
+
return inline.slice(flag.length + 1);
|
|
20
|
+
const index = argv.indexOf(flag);
|
|
21
|
+
if (index !== -1 && index + 1 < argv.length)
|
|
22
|
+
return argv[index + 1];
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
/** Parses skills CLI flags, ignoring the leading `skills` subcommand token. */
|
|
26
|
+
function parseFlags(argv) {
|
|
27
|
+
const args = argv.filter((arg) => arg !== 'skills');
|
|
28
|
+
const providers = parseList(readFlagValue(args, '--providers')).filter((value) => ALL_PROVIDERS.includes(value));
|
|
29
|
+
const layers = parseList(readFlagValue(args, '--layer') ?? readFlagValue(args, '--layers')).filter((value) => ALL_LAYERS.includes(value));
|
|
30
|
+
return {
|
|
31
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
32
|
+
yes: args.includes('--yes') || args.includes('-y'),
|
|
33
|
+
dryRun: args.includes('--dry-run'),
|
|
34
|
+
force: args.includes('--force'),
|
|
35
|
+
global: args.includes('--global'),
|
|
36
|
+
providers: providers.length > 0 ? providers : undefined,
|
|
37
|
+
layers: layers.length > 0 ? layers : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** One-line summary of what an action means, for the preview. */
|
|
41
|
+
function actionLabel(action) {
|
|
42
|
+
switch (action) {
|
|
43
|
+
case 'create':
|
|
44
|
+
return ui.success('create');
|
|
45
|
+
case 'update':
|
|
46
|
+
return ui.info('update');
|
|
47
|
+
case 'unchanged':
|
|
48
|
+
return ui.dim('unchanged');
|
|
49
|
+
case 'skip-foreign':
|
|
50
|
+
return ui.warning('skip (foreign file)');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Prints the planned writes so the user can review before anything is written. */
|
|
54
|
+
function printPreview(plans) {
|
|
55
|
+
printHeading('Planned changes');
|
|
56
|
+
printRule();
|
|
57
|
+
for (const plan of plans) {
|
|
58
|
+
const { provider, layer, scope } = plan.target;
|
|
59
|
+
console.log(` ${actionLabel(plan.action).padEnd(28)} ${ui.bold(`${provider}/${layer}`)} ${ui.dim(`(${scope})`)}`);
|
|
60
|
+
console.log(` ${ui.dim('→ ' + plan.target.path)}`);
|
|
61
|
+
}
|
|
62
|
+
printRule();
|
|
63
|
+
}
|
|
64
|
+
/** Prints CLI usage. */
|
|
65
|
+
function printHelp() {
|
|
66
|
+
console.log(`
|
|
67
|
+
${ui.bold('ebay-mcp skills')} ${ui.dim('install AI skills for Codex, Claude Code, and Cursor')}
|
|
68
|
+
|
|
69
|
+
${ui.bold('Usage:')}
|
|
70
|
+
ebay-mcp skills [options]
|
|
71
|
+
npm run skills -- [options]
|
|
72
|
+
|
|
73
|
+
${ui.bold('Options:')}
|
|
74
|
+
-h, --help Show this help
|
|
75
|
+
-y, --yes Non-interactive; accept detected defaults and flags
|
|
76
|
+
--providers <list> Comma list: claude,cursor,codex
|
|
77
|
+
--layer <list> Comma list: using,contributing (default: using)
|
|
78
|
+
--global Install to your home dir instead of this project
|
|
79
|
+
--dry-run Show planned changes without writing
|
|
80
|
+
--force Overwrite a same-named file we did not generate
|
|
81
|
+
|
|
82
|
+
${ui.bold('Examples:')}
|
|
83
|
+
ebay-mcp skills
|
|
84
|
+
ebay-mcp skills --providers cursor,codex --layer using --dry-run
|
|
85
|
+
ebay-mcp skills --yes
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
/** Resolves the providers to use, preferring flags, else detection at the scope. */
|
|
89
|
+
function resolveDefaultProviders(scope, cwd, home) {
|
|
90
|
+
return ALL_PROVIDERS.filter((provider) => detectProvider(provider, scope, cwd, home));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Runs the skills installer end to end: detect → choose providers/layers/scope →
|
|
94
|
+
* preview → confirm → write. Interactive by default; non-interactive when `--yes`
|
|
95
|
+
* is passed or stdin is not a TTY (so `npx ebay-mcp skills` in CI won't hang).
|
|
96
|
+
*
|
|
97
|
+
* Shared by the `ebay-mcp skills` subcommand, `npm run skills`, and the optional
|
|
98
|
+
* step at the end of `npm run setup`.
|
|
99
|
+
*
|
|
100
|
+
* @param options `argv` overrides process args; `fromSetup` trims the banner.
|
|
101
|
+
*/
|
|
102
|
+
export async function runSkillsWizard(options = {}) {
|
|
103
|
+
const flags = parseFlags(options.argv ?? process.argv.slice(2));
|
|
104
|
+
if (flags.help) {
|
|
105
|
+
printHelp();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? cwd;
|
|
110
|
+
const snapshot = buildRegistrySnapshot();
|
|
111
|
+
const nonInteractive = flags.yes || !process.stdin.isTTY;
|
|
112
|
+
if (!options.fromSetup) {
|
|
113
|
+
console.log(`\n ${ebayPalette.blue.bold('eBay MCP')} ${ui.bold('· AI skills installer')}`);
|
|
114
|
+
console.log(` ${ui.dim(`Generate skills for Codex, Claude Code, and Cursor from ${snapshot.toolCount} live tools.`)}\n`);
|
|
115
|
+
}
|
|
116
|
+
let providers;
|
|
117
|
+
let layers;
|
|
118
|
+
let scope;
|
|
119
|
+
if (nonInteractive) {
|
|
120
|
+
scope = flags.global ? 'global' : 'project';
|
|
121
|
+
providers = flags.providers ?? resolveDefaultProviders(scope, cwd, home);
|
|
122
|
+
layers = flags.layers ?? ['using'];
|
|
123
|
+
if (providers.length === 0) {
|
|
124
|
+
printWarning('No AI tools detected and none specified (--providers). Nothing to do.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
printInfo(`Non-interactive: ${providers.join(', ')} · ${layers.join(', ')} · ${scope}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const detectedProject = resolveDefaultProviders('project', cwd, home);
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
const onCancel = () => {
|
|
133
|
+
cancelled = true;
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
const answers = (await prompts([
|
|
137
|
+
{
|
|
138
|
+
type: 'multiselect',
|
|
139
|
+
name: 'providers',
|
|
140
|
+
message: 'Install skills for which AI tools?',
|
|
141
|
+
instructions: false,
|
|
142
|
+
choices: ALL_PROVIDERS.map((provider) => ({
|
|
143
|
+
title: providerLabel(provider),
|
|
144
|
+
value: provider,
|
|
145
|
+
selected: flags.providers
|
|
146
|
+
? flags.providers.includes(provider)
|
|
147
|
+
: detectedProject.includes(provider),
|
|
148
|
+
})),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'multiselect',
|
|
152
|
+
name: 'layers',
|
|
153
|
+
message: 'Which skills?',
|
|
154
|
+
instructions: false,
|
|
155
|
+
choices: [
|
|
156
|
+
{ title: 'Using — drive the eBay tools', value: 'using', selected: true },
|
|
157
|
+
{
|
|
158
|
+
title: 'Contributing — work on the ebay-mcp repo',
|
|
159
|
+
value: 'contributing',
|
|
160
|
+
selected: flags.layers?.includes('contributing') ?? false,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'select',
|
|
166
|
+
name: 'scope',
|
|
167
|
+
message: 'Where should they be installed?',
|
|
168
|
+
choices: [
|
|
169
|
+
{ title: 'This project (recommended)', value: 'project' },
|
|
170
|
+
{ title: 'Global — my home dir (Cursor stays project-local)', value: 'global' },
|
|
171
|
+
],
|
|
172
|
+
initial: flags.global ? 1 : 0,
|
|
173
|
+
},
|
|
174
|
+
], { onCancel }));
|
|
175
|
+
if (cancelled) {
|
|
176
|
+
printWarning('Cancelled — nothing written.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
providers = answers.providers ?? [];
|
|
180
|
+
layers = answers.layers ?? [];
|
|
181
|
+
scope = answers.scope ?? 'project';
|
|
182
|
+
if (providers.length === 0 || layers.length === 0) {
|
|
183
|
+
printWarning('Nothing selected — nothing written.');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const rendered = renderSkillTargets({ providers, layers, scope, cwd, home, snapshot });
|
|
188
|
+
const plans = rendered.map((item) => planWrite(item.target, item.payload));
|
|
189
|
+
printPreview(plans);
|
|
190
|
+
if (flags.dryRun) {
|
|
191
|
+
printInfo('Dry run — no files written.');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const foreign = plans.filter((plan) => plan.action === 'skip-foreign');
|
|
195
|
+
if (foreign.length > 0 && !flags.force) {
|
|
196
|
+
printWarning(`${foreign.length} file(s) exist that this tool did not generate and will be skipped. Re-run with --force to overwrite.`);
|
|
197
|
+
}
|
|
198
|
+
if (!nonInteractive) {
|
|
199
|
+
const writable = plans.filter((plan) => plan.action !== 'unchanged' && !(plan.action === 'skip-foreign' && !flags.force)).length;
|
|
200
|
+
if (writable === 0) {
|
|
201
|
+
printInfo('Everything is already up to date.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let cancelled = false;
|
|
205
|
+
const { go } = (await prompts({ type: 'confirm', name: 'go', message: `Write ${writable} file(s)?`, initial: true }, {
|
|
206
|
+
onCancel: () => {
|
|
207
|
+
cancelled = true;
|
|
208
|
+
return false;
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
if (cancelled || !go) {
|
|
212
|
+
printWarning('Cancelled — nothing written.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Re-plan each write against current disk state so that two managed-block
|
|
217
|
+
// targets sharing one file (both Codex layers → one AGENTS.md) compose instead
|
|
218
|
+
// of the second clobbering the first.
|
|
219
|
+
let written = 0;
|
|
220
|
+
for (const item of rendered) {
|
|
221
|
+
const plan = planWrite(item.target, item.payload);
|
|
222
|
+
const result = applyWrite(plan, { dryRun: false, force: flags.force });
|
|
223
|
+
if (result.written) {
|
|
224
|
+
written += 1;
|
|
225
|
+
printSuccess(`${result.action} ${plan.target.path}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
printHeading('Done');
|
|
229
|
+
printInfo(`${written} file(s) written. Restart your AI tool so it picks up the new skills.`);
|
|
230
|
+
}
|
|
231
|
+
const entryPath = process.argv[1] ? resolve(process.argv[1]) : undefined;
|
|
232
|
+
const modulePath = resolve(fileURLToPath(import.meta.url));
|
|
233
|
+
if (entryPath && modulePath === entryPath) {
|
|
234
|
+
runSkillsWizard().catch((error) => {
|
|
235
|
+
console.error(ui.error('\n Skills install failed:'), error);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Renders the live family index as a Markdown table. */
|
|
2
|
+
function familyTable(snapshot) {
|
|
3
|
+
const rows = snapshot.families
|
|
4
|
+
.map((family) => `| ${family.title} | ${family.count} | ${family.blurb} |`)
|
|
5
|
+
.join('\n');
|
|
6
|
+
return ['| Family | Tools | Covers |', '| --- | --- | --- |', rows].join('\n');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds the "using" skill: how a developer's AI should drive the eBay MCP
|
|
10
|
+
* tools. Workflow-first and lean — the per-tool schemas are already visible via
|
|
11
|
+
* MCP `tools/list`, so this focuses on sequencing, auth, and disambiguation that
|
|
12
|
+
* tool discovery can't convey. The family index is injected live from the
|
|
13
|
+
* registry snapshot.
|
|
14
|
+
*
|
|
15
|
+
* @param snapshot Live tool count and family index.
|
|
16
|
+
* @returns The provider-neutral skill document.
|
|
17
|
+
*/
|
|
18
|
+
export function buildUsingDoc(snapshot) {
|
|
19
|
+
return {
|
|
20
|
+
slug: 'ebay-mcp-using',
|
|
21
|
+
title: 'Using the eBay MCP tools',
|
|
22
|
+
description: `Drive eBay's Sell APIs through the ebay-mcp server: ${snapshot.toolCount} tools for listings, orders, marketing, and analytics. Use when creating or revising listings, fulfilling orders, issuing refunds, running Promoted Listings campaigns, or debugging eBay auth/rate-limit errors.`,
|
|
23
|
+
intro: `The \`ebay-mcp\` server exposes **${snapshot.toolCount} tools across 100% of eBay's Sell APIs**, running locally over MCP. Every tool is named \`ebay_<verb>_<noun>\` (plus two ChatGPT-connector tools, \`search\`/\`fetch\`). You already see each tool's input schema via \`tools/list\` — this skill covers what discovery can't: which tools to chain, what eBay requires first, and how to read its errors.`,
|
|
24
|
+
sections: [
|
|
25
|
+
{
|
|
26
|
+
heading: 'Authentication & environment',
|
|
27
|
+
body: [
|
|
28
|
+
'- **Two auth tiers.** An app token allows ≈1,000 requests/day; user OAuth allows ≈10k–50k/day. Most write operations require user OAuth.',
|
|
29
|
+
'- **A 401 / "access denied" is almost always a scope or environment problem**, not a malformed request. Check `ebay_get_token_status`, then re-run `npm run setup` if a scope is missing.',
|
|
30
|
+
'- **Sandbox and production are separate worlds** — separate credentials and separate data. Confirm `EBAY_ENVIRONMENT` before investigating "missing" data.',
|
|
31
|
+
'- **Marketplace & language headers matter.** Calls honor `EBAY_MARKETPLACE_ID` (e.g. `EBAY_US`) and `Content-Language` (e.g. `en-US`); a wrong pair causes empty or rejected results.',
|
|
32
|
+
].join('\n'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
heading: 'Core workflows',
|
|
36
|
+
body: [
|
|
37
|
+
'**Publish a fixed-price listing (REST Inventory model).**',
|
|
38
|
+
'1. One-time account setup: `ebay_opt_in_to_program` (business-policy management), then `ebay_create_payment_policy`, `ebay_create_return_policy`, `ebay_create_fulfillment_policy` for reusable policy IDs, and `ebay_create_or_replace_inventory_location` for a `merchantLocationKey`.',
|
|
39
|
+
'2. `ebay_create_inventory_item` (SKU + product details + availability).',
|
|
40
|
+
'3. `ebay_create_offer` (SKU, marketplace, price, the policy IDs, location).',
|
|
41
|
+
'4. `ebay_publish_offer` (offerId) → a live `listingId`. Use `ebay_bulk_publish_offer` for many at once.',
|
|
42
|
+
'',
|
|
43
|
+
'**Pick the right category & required item specifics first.**',
|
|
44
|
+
'`ebay_get_default_category_tree_id` (marketplace) → `ebay_get_category_suggestions` (query) → `ebay_get_item_aspects_for_category` to learn the required aspects to put on the inventory item.',
|
|
45
|
+
'',
|
|
46
|
+
'**Fulfill an order.**',
|
|
47
|
+
'`ebay_get_orders` (filter unfulfilled) → `ebay_get_order` (detail) → `ebay_create_shipping_fulfillment` (tracking number + carrier).',
|
|
48
|
+
'',
|
|
49
|
+
'**Refund or handle a payment dispute.**',
|
|
50
|
+
'Voluntary refund: `ebay_issue_refund`. Disputes: `ebay_get_payment_dispute_summaries` → `ebay_get_payment_dispute` → `ebay_accept_payment_dispute` (or contest it).',
|
|
51
|
+
'',
|
|
52
|
+
'**Promote listings (Promoted Listings).**',
|
|
53
|
+
'`ebay_create_campaign` → `ebay_bulk_create_ads_by_inventory_reference`. The Marketing family also covers promotions and reports.',
|
|
54
|
+
'',
|
|
55
|
+
'**Legacy XML path (Trading API).**',
|
|
56
|
+
'`ebay_create_listing` / `ebay_revise_listing` / `ebay_relist_item` / `ebay_end_listing`, list via `ebay_get_active_listings`. Use only when you specifically need the legacy flow — do not mix it with the REST Inventory model for the same SKU.',
|
|
57
|
+
'',
|
|
58
|
+
'**Diagnose failing calls.**',
|
|
59
|
+
'`ebay_get_token_status` (auth valid/expiring?) → `ebay_get_rate_limits` / `ebay_get_user_rate_limits` (quota hit?) → `ebay_get_api_status` (eBay-side outage?).',
|
|
60
|
+
].join('\n'),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
heading: 'Gotchas & disambiguation',
|
|
64
|
+
body: [
|
|
65
|
+
'- **Two listing models.** REST Inventory (`inventory_item` → `offer` → `publish`) vs legacy Trading (XML). Pick one per SKU and stay in it.',
|
|
66
|
+
'- **Offers need policies + a location first.** A "policy not found" error usually means the one-time account setup was skipped.',
|
|
67
|
+
'- **`search` and `fetch` are ChatGPT-connector only** — ignore them when driving the Sell API directly.',
|
|
68
|
+
'- **Write actions are real.** `publish`, `issue_refund`, `end_listing`, and any `bulk_*` change live seller data — confirm intent before calling.',
|
|
69
|
+
].join('\n'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
heading: `Tool families (${snapshot.toolCount} tools)`,
|
|
73
|
+
body: familyTable(snapshot),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds the "contributing" skill: how an AI working on the ebay-mcp repo should
|
|
80
|
+
* operate. Mirrors AGENTS.md (validation commands, module map, the add-a-tool
|
|
81
|
+
* flow, conventions) against the real `src/tools/categories/` layout.
|
|
82
|
+
*
|
|
83
|
+
* @param snapshot Live tool count and family index.
|
|
84
|
+
* @returns The provider-neutral skill document.
|
|
85
|
+
*/
|
|
86
|
+
export function buildContributingDoc(snapshot) {
|
|
87
|
+
return {
|
|
88
|
+
slug: 'ebay-mcp-contributing',
|
|
89
|
+
title: 'Contributing to ebay-mcp',
|
|
90
|
+
description: `Work on the ebay-mcp server itself: ${snapshot.toolCount} tools across eBay's Sell APIs (TypeScript/ESM, Zod, OpenAPI-generated types). Use when adding or changing eBay tools/endpoints, wiring the registry, or running the project's checks.`,
|
|
91
|
+
intro: `A local MCP server exposing **${snapshot.toolCount} tools across 100% of eBay's Sell APIs** — TypeScript/Node.js (ESM), \`@modelcontextprotocol/sdk\`, Zod validation, OpenAPI-generated types. Entry points: \`src/index.ts\` (STDIO) and \`src/server-http.ts\` (HTTP).`,
|
|
92
|
+
sections: [
|
|
93
|
+
{
|
|
94
|
+
heading: 'Validation (run before a PR)',
|
|
95
|
+
body: [
|
|
96
|
+
'```bash',
|
|
97
|
+
'npm run check # tsc --noEmit + eslint + prettier --check (must pass)',
|
|
98
|
+
'npm test # vitest run',
|
|
99
|
+
'npm run build # tsc + tsc-alias → build/',
|
|
100
|
+
'```',
|
|
101
|
+
].join('\n'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
heading: 'Module map (`src/`)',
|
|
105
|
+
body: [
|
|
106
|
+
'| Path | Owns |',
|
|
107
|
+
'| --- | --- |',
|
|
108
|
+
'| `index.ts` / `server-http.ts` | MCP entry points (STDIO / HTTP) |',
|
|
109
|
+
'| `api/` | eBay API client implementations (one area per file) |',
|
|
110
|
+
'| `auth/` | OAuth 2.0 flow and token management |',
|
|
111
|
+
'| `config/` | Environment loading, constants, marketplace defaults |',
|
|
112
|
+
'| `tools/` | Tool wiring — `registry.ts`, `contracts.ts`, `define-tool.ts`, and `categories/` (13 family files that co-locate each tool definition with its handler via `defineTool`) |',
|
|
113
|
+
"| `skills/` | Agent-skills generator (this skill's source) |",
|
|
114
|
+
'| `schemas/` | Shared Zod schemas |',
|
|
115
|
+
"| `types/` | TypeScript types — **auto-generated** from OpenAPI specs (don't hand-edit) |",
|
|
116
|
+
'| `scripts/` | CLI tooling: `setup.ts`, `skills.ts`, `dev-sync.ts`, `diagnostics.ts` |',
|
|
117
|
+
'| `utils/` | Shared utilities (logging, http, errors) |',
|
|
118
|
+
].join('\n'),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
heading: 'Add a tool or endpoint',
|
|
122
|
+
body: [
|
|
123
|
+
'1. `npm run sync` — download the latest eBay specs, regenerate types, report missing endpoints.',
|
|
124
|
+
'2. Add the API method in `src/api/`.',
|
|
125
|
+
'3. Add a `defineTool({ ... handler })` entry in the matching `src/tools/categories/<family>.ts` — definition and handler live together; `registry.ts` derives everything from `categories/index.ts`.',
|
|
126
|
+
'4. Add tests in `tests/`.',
|
|
127
|
+
'5. `npm run check && npm test`.',
|
|
128
|
+
].join('\n'),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
heading: 'Conventions',
|
|
132
|
+
body: [
|
|
133
|
+
'- **No `any`** — specific types; prefer narrowing over assertions. `types/` is generated, so model new shapes from the specs.',
|
|
134
|
+
'- Validate tool inputs with Zod; derive related schemas rather than duplicating fields.',
|
|
135
|
+
'- Commit with Conventional Commits (releases are changeset-driven).',
|
|
136
|
+
'- Logs go to **stderr** only — stdout is reserved for the MCP protocol.',
|
|
137
|
+
].join('\n'),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildRegistrySnapshot } from '../skills/registry-snapshot.js';
|
|
2
|
+
import { buildContributingDoc, buildUsingDoc } from '../skills/content.js';
|
|
3
|
+
import { renderSkill } from '../skills/render.js';
|
|
4
|
+
import { resolveTargets } from '../skills/targets.js';
|
|
5
|
+
import { planWrite } from '../skills/install.js';
|
|
6
|
+
export * from '../skills/types.js';
|
|
7
|
+
export { buildRegistrySnapshot } from '../skills/registry-snapshot.js';
|
|
8
|
+
export { buildUsingDoc, buildContributingDoc } from '../skills/content.js';
|
|
9
|
+
export { renderSkill, renderClaudeSkill, renderCursorRule, renderCodexSection, } from '../skills/render.js';
|
|
10
|
+
export { ALL_PROVIDERS, providerLabel, supportsScope, detectProvider, resolveTargets, } from '../skills/targets.js';
|
|
11
|
+
export { planWrite, applyWrite } from '../skills/install.js';
|
|
12
|
+
/**
|
|
13
|
+
* Returns the canonical document for a layer with the live registry injected.
|
|
14
|
+
*
|
|
15
|
+
* @param layer Which audience the skill serves.
|
|
16
|
+
* @param snapshot Live registry snapshot (defaults to a fresh one).
|
|
17
|
+
*/
|
|
18
|
+
export function buildSkillDoc(layer, snapshot = buildRegistrySnapshot()) {
|
|
19
|
+
return layer === 'using' ? buildUsingDoc(snapshot) : buildContributingDoc(snapshot);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The end-to-end core: resolves targets for the chosen providers × layers,
|
|
23
|
+
* renders each into its provider's native format, and returns idempotent write
|
|
24
|
+
* plans — without touching disk. Both the wizard and tests build on this.
|
|
25
|
+
*
|
|
26
|
+
* @param options Providers, layers, scope, and optional cwd/home/snapshot overrides.
|
|
27
|
+
* @returns One {@link RenderedTarget} per provider/layer target.
|
|
28
|
+
*/
|
|
29
|
+
export function renderSkillTargets(options) {
|
|
30
|
+
const snapshot = options.snapshot ?? buildRegistrySnapshot();
|
|
31
|
+
const docByLayer = {
|
|
32
|
+
using: buildUsingDoc(snapshot),
|
|
33
|
+
contributing: buildContributingDoc(snapshot),
|
|
34
|
+
};
|
|
35
|
+
const targets = resolveTargets(options.providers, options.layers, options.scope, options.cwd, options.home);
|
|
36
|
+
return targets.map((target) => ({
|
|
37
|
+
target,
|
|
38
|
+
payload: renderSkill(target.provider, docByLayer[target.layer], target.layer),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds preview write plans for the chosen providers × layers. Note: when two
|
|
43
|
+
* managed-block targets share a file (both Codex layers → one `AGENTS.md`), each
|
|
44
|
+
* plan here is computed against the same on-disk content, so apply each write
|
|
45
|
+
* by re-planning from {@link RenderedTarget} (see the wizard) rather than
|
|
46
|
+
* applying these plans directly — otherwise the second same-file write would not
|
|
47
|
+
* see the first.
|
|
48
|
+
*
|
|
49
|
+
* @param options Providers, layers, scope, and optional cwd/home/snapshot overrides.
|
|
50
|
+
* @returns One {@link WritePlan} per provider/layer target.
|
|
51
|
+
*/
|
|
52
|
+
export function buildSkillPlans(options) {
|
|
53
|
+
return renderSkillTargets(options).map((rendered) => planWrite(rendered.target, rendered.payload));
|
|
54
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { blockEnd, blockStart, isOwnedByUs } from '../skills/markers.js';
|
|
4
|
+
/** Wraps managed-block inner content between this layer's delimiters. */
|
|
5
|
+
function wrapBlock(layer, inner) {
|
|
6
|
+
return `${blockStart(layer)}\n${inner}\n${blockEnd(layer)}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Replaces our managed block in `existing`, or appends it if absent — preserving
|
|
10
|
+
* all surrounding user content. Idempotent: feeding the result back in produces
|
|
11
|
+
* identical output.
|
|
12
|
+
*/
|
|
13
|
+
function mergeManagedBlock(existing, layer, inner) {
|
|
14
|
+
const start = blockStart(layer);
|
|
15
|
+
const end = blockEnd(layer);
|
|
16
|
+
const block = wrapBlock(layer, inner);
|
|
17
|
+
const startIdx = existing.indexOf(start);
|
|
18
|
+
const endIdx = existing.indexOf(end);
|
|
19
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
20
|
+
return existing.slice(0, startIdx) + block + existing.slice(endIdx + end.length);
|
|
21
|
+
}
|
|
22
|
+
const prefix = existing.trim().length > 0 ? `${existing.replace(/\s+$/, '')}\n\n` : '';
|
|
23
|
+
return `${prefix}${block}\n`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Computes what a write would do without touching disk.
|
|
27
|
+
*
|
|
28
|
+
* - Owned files (Claude/Cursor): `create` when absent, `update`/`unchanged` when
|
|
29
|
+
* ours, `skip-foreign` when a same-named file exists that we did not write.
|
|
30
|
+
* - Managed blocks (Codex `AGENTS.md`): the block is merged into existing
|
|
31
|
+
* content, so the result is `create`/`update`/`unchanged` and user text is
|
|
32
|
+
* always preserved (never `skip-foreign`).
|
|
33
|
+
*
|
|
34
|
+
* @param target Resolved destination.
|
|
35
|
+
* @param payload Rendered content — a full file for owned targets, inner block
|
|
36
|
+
* content for managed targets.
|
|
37
|
+
* @returns The planned action and the exact bytes that would be written.
|
|
38
|
+
*/
|
|
39
|
+
export function planWrite(target, payload) {
|
|
40
|
+
const exists = existsSync(target.path);
|
|
41
|
+
const previousContents = exists ? readFileSync(target.path, 'utf-8') : undefined;
|
|
42
|
+
if (target.kind === 'managed-block') {
|
|
43
|
+
const nextContents = mergeManagedBlock(previousContents ?? '', target.layer, payload);
|
|
44
|
+
const action = !exists ? 'create' : nextContents === previousContents ? 'unchanged' : 'update';
|
|
45
|
+
return { target, action, nextContents, previousContents };
|
|
46
|
+
}
|
|
47
|
+
// owned-file
|
|
48
|
+
if (!exists) {
|
|
49
|
+
return { target, action: 'create', nextContents: payload };
|
|
50
|
+
}
|
|
51
|
+
if (!isOwnedByUs(previousContents ?? '', target.layer)) {
|
|
52
|
+
return { target, action: 'skip-foreign', nextContents: payload, previousContents };
|
|
53
|
+
}
|
|
54
|
+
const action = previousContents === payload ? 'unchanged' : 'update';
|
|
55
|
+
return { target, action, nextContents: payload, previousContents };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Applies a write plan. Skips no-op writes (`unchanged`) and refuses to clobber
|
|
59
|
+
* a foreign owned file unless `force` is set. `dryRun` computes the result
|
|
60
|
+
* without writing.
|
|
61
|
+
*
|
|
62
|
+
* @param plan The plan from {@link planWrite}.
|
|
63
|
+
* @param options `dryRun` to preview only; `force` to overwrite a foreign file.
|
|
64
|
+
* @returns Whether the file was written.
|
|
65
|
+
*/
|
|
66
|
+
export function applyWrite(plan, options = {}) {
|
|
67
|
+
const isNoop = plan.action === 'unchanged';
|
|
68
|
+
const isBlockedForeign = plan.action === 'skip-foreign' && !options.force;
|
|
69
|
+
if (options.dryRun || isNoop || isBlockedForeign) {
|
|
70
|
+
return { target: plan.target, action: plan.action, written: false };
|
|
71
|
+
}
|
|
72
|
+
mkdirSync(dirname(plan.target.path), { recursive: true });
|
|
73
|
+
writeFileSync(plan.target.path, plan.nextContents, 'utf-8');
|
|
74
|
+
return { target: plan.target, action: plan.action, written: true };
|
|
75
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML-comment markers that tag generated content as ours. Both owned files
|
|
3
|
+
* (Claude `SKILL.md`, Cursor `.mdc`) and managed blocks (Codex `AGENTS.md`) use
|
|
4
|
+
* the same layer-scoped namespace so re-runs find and replace exactly what a
|
|
5
|
+
* prior run wrote — and never touch a user's own content.
|
|
6
|
+
*
|
|
7
|
+
* Markers are HTML comments, which render invisibly in Markdown across all three
|
|
8
|
+
* tools.
|
|
9
|
+
*/
|
|
10
|
+
/** Single-line marker embedded near the top of an owned file to prove ownership. */
|
|
11
|
+
export function ownershipMarker(layer) {
|
|
12
|
+
return `<!-- ebay-mcp:skill:${layer} (generated — re-run \`ebay-mcp skills\` to update) -->`;
|
|
13
|
+
}
|
|
14
|
+
/** Opening delimiter of a managed block inside a shared file. */
|
|
15
|
+
export function blockStart(layer) {
|
|
16
|
+
return `<!-- ebay-mcp:skill:${layer}:start -->`;
|
|
17
|
+
}
|
|
18
|
+
/** Closing delimiter of a managed block inside a shared file. */
|
|
19
|
+
export function blockEnd(layer) {
|
|
20
|
+
return `<!-- ebay-mcp:skill:${layer}:end -->`;
|
|
21
|
+
}
|
|
22
|
+
/** True when file contents already contain content we generated for this layer. */
|
|
23
|
+
export function isOwnedByUs(contents, layer) {
|
|
24
|
+
return contents.includes(`ebay-mcp:skill:${layer}`); // matches ownership marker or block delimiters
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { toolCategories } from '../tools/categories/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Human descriptions for each tool family, keyed by the category `key` exported
|
|
4
|
+
* from the registry. Kept here (next to the snapshot) so copy lives in one place
|
|
5
|
+
* while counts stay live; an unknown key falls back to its registry title.
|
|
6
|
+
*/
|
|
7
|
+
const FAMILY_BLURBS = {
|
|
8
|
+
connector: 'ChatGPT connector protocol tools (`search`/`fetch`) — not used when driving the API directly',
|
|
9
|
+
'token-management': 'OAuth URL, token status, refresh, and credential diagnostics',
|
|
10
|
+
account: 'Business policies (payment, return, fulfillment), privileges, program opt-in, sales tax',
|
|
11
|
+
inventory: 'Inventory items, offers, locations, inventory groups, bulk publish — the REST listing model',
|
|
12
|
+
fulfillment: 'Orders, shipping fulfillments, refunds, and payment disputes',
|
|
13
|
+
marketing: 'Promoted Listings campaigns, ads, promotions, and marketing reports',
|
|
14
|
+
analytics: 'Seller standards, traffic reports, and customer-service metrics',
|
|
15
|
+
metadata: 'Marketplace policies, item conditions, listing constraints, automotive compatibility',
|
|
16
|
+
taxonomy: 'Category trees, category suggestions, and required item aspects',
|
|
17
|
+
communication: 'Buyer messages, member messages, and notification settings',
|
|
18
|
+
other: 'Feedback, recommendations, and assorted Sell-API helpers',
|
|
19
|
+
developer: 'API status, rate limits, client registration, and signing keys',
|
|
20
|
+
trading: 'Legacy Trading API (XML) — create / revise / relist / end fixed-price listings',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Builds a {@link RegistrySnapshot} from the live tool registry so a rendered
|
|
24
|
+
* skill always reflects the real catalogue. Counts come from {@link toolCategories}
|
|
25
|
+
* (the single source of truth for registry families); blurbs are static copy.
|
|
26
|
+
*
|
|
27
|
+
* @returns The current tool count and per-family index, in registry order.
|
|
28
|
+
*/
|
|
29
|
+
export function buildRegistrySnapshot() {
|
|
30
|
+
const families = toolCategories.map((category) => ({
|
|
31
|
+
key: category.key,
|
|
32
|
+
title: category.title,
|
|
33
|
+
count: category.entries.length,
|
|
34
|
+
blurb: FAMILY_BLURBS[category.key] ?? category.title,
|
|
35
|
+
}));
|
|
36
|
+
const toolCount = families.reduce((sum, family) => sum + family.count, 0);
|
|
37
|
+
return { toolCount, families };
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ownershipMarker } from '../skills/markers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Composes the shared Markdown body (title + intro + sections) at a given
|
|
4
|
+
* heading level. `baseLevel` 1 yields a standalone document (`#` title, `##`
|
|
5
|
+
* sections) for owned files; level 2 yields an embeddable section (`##` title,
|
|
6
|
+
* `###` sections) for a managed block inside a user's file.
|
|
7
|
+
*/
|
|
8
|
+
function renderBody(doc, baseLevel) {
|
|
9
|
+
const heading = (level) => '#'.repeat(level);
|
|
10
|
+
const parts = [`${heading(baseLevel)} ${doc.title}`, '', doc.intro];
|
|
11
|
+
for (const section of doc.sections) {
|
|
12
|
+
parts.push('', `${heading(baseLevel + 1)} ${section.heading}`, '', section.body);
|
|
13
|
+
}
|
|
14
|
+
return parts.join('\n');
|
|
15
|
+
}
|
|
16
|
+
/** YAML frontmatter scalar that is safe for arbitrary text (JSON strings are valid YAML). */
|
|
17
|
+
function yamlString(value) {
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Renders a Claude Code skill file (`SKILL.md`): YAML frontmatter (`name`,
|
|
22
|
+
* `description`) so the model can invoke it on demand, an ownership marker, then
|
|
23
|
+
* the shared body.
|
|
24
|
+
*/
|
|
25
|
+
export function renderClaudeSkill(doc, layer) {
|
|
26
|
+
const frontmatter = [
|
|
27
|
+
'---',
|
|
28
|
+
`name: ${doc.slug}`,
|
|
29
|
+
`description: ${yamlString(doc.description)}`,
|
|
30
|
+
'---',
|
|
31
|
+
];
|
|
32
|
+
return [...frontmatter, ownershipMarker(layer), '', renderBody(doc, 1), ''].join('\n');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Renders a Cursor rule file (`.mdc`): frontmatter with a `description` trigger
|
|
36
|
+
* and `alwaysApply: false` (agent-requested, not file-glob scoped, since eBay
|
|
37
|
+
* tools aren't tied to particular source files), an ownership marker, then body.
|
|
38
|
+
*/
|
|
39
|
+
export function renderCursorRule(doc, layer) {
|
|
40
|
+
const frontmatter = [
|
|
41
|
+
'---',
|
|
42
|
+
`description: ${yamlString(doc.description)}`,
|
|
43
|
+
'alwaysApply: false',
|
|
44
|
+
'---',
|
|
45
|
+
];
|
|
46
|
+
return [...frontmatter, ownershipMarker(layer), '', renderBody(doc, 1), ''].join('\n');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Renders the inner content for a Codex `AGENTS.md` managed block: a level-2
|
|
50
|
+
* section with no frontmatter, so it nests cleanly under a user's existing
|
|
51
|
+
* top-level heading. The install step wraps this with block delimiters.
|
|
52
|
+
*/
|
|
53
|
+
export function renderCodexSection(doc) {
|
|
54
|
+
return renderBody(doc, 2);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Renders the payload for one provider. Owned-file providers (Claude, Cursor)
|
|
58
|
+
* return a complete file; the managed-block provider (Codex) returns inner
|
|
59
|
+
* content that {@link import('./install.js')} wraps with markers.
|
|
60
|
+
*
|
|
61
|
+
* @param provider Target tool.
|
|
62
|
+
* @param doc Canonical skill document.
|
|
63
|
+
* @param layer Skill layer (for the ownership marker).
|
|
64
|
+
* @returns The string payload for this provider.
|
|
65
|
+
*/
|
|
66
|
+
export function renderSkill(provider, doc, layer) {
|
|
67
|
+
switch (provider) {
|
|
68
|
+
case 'claude':
|
|
69
|
+
return renderClaudeSkill(doc, layer);
|
|
70
|
+
case 'cursor':
|
|
71
|
+
return renderCursorRule(doc, layer);
|
|
72
|
+
case 'codex':
|
|
73
|
+
return renderCodexSection(doc);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
/** All providers we can render for, in display order. */
|
|
5
|
+
export const ALL_PROVIDERS = ['claude', 'cursor', 'codex'];
|
|
6
|
+
/** Human label for a provider, used in the wizard and reports. */
|
|
7
|
+
export function providerLabel(provider) {
|
|
8
|
+
switch (provider) {
|
|
9
|
+
case 'claude':
|
|
10
|
+
return 'Claude Code (.claude/skills)';
|
|
11
|
+
case 'cursor':
|
|
12
|
+
return 'Cursor (.cursor/rules)';
|
|
13
|
+
case 'codex':
|
|
14
|
+
return 'Codex (AGENTS.md)';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether a provider supports a given scope. Cursor only reads project-local
|
|
19
|
+
* rule files, so it has no global scope.
|
|
20
|
+
*/
|
|
21
|
+
export function supportsScope(provider, scope) {
|
|
22
|
+
if (provider === 'cursor')
|
|
23
|
+
return scope === 'project';
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/** The filename slug for a layer, shared by Claude skill dirs and Cursor rule files. */
|
|
27
|
+
function slug(layer) {
|
|
28
|
+
return `ebay-mcp-${layer}`;
|
|
29
|
+
}
|
|
30
|
+
/** Absolute path of the rendered file for a provider/layer/scope. */
|
|
31
|
+
function resolvePath(provider, layer, scope, cwd, home) {
|
|
32
|
+
const base = scope === 'project' ? cwd : home;
|
|
33
|
+
switch (provider) {
|
|
34
|
+
case 'claude':
|
|
35
|
+
return join(base, '.claude', 'skills', slug(layer), 'SKILL.md');
|
|
36
|
+
case 'cursor':
|
|
37
|
+
return join(cwd, '.cursor', 'rules', `${slug(layer)}.mdc`);
|
|
38
|
+
case 'codex':
|
|
39
|
+
return scope === 'project' ? join(cwd, 'AGENTS.md') : join(home, '.codex', 'AGENTS.md');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Codex shares one `AGENTS.md` (managed block); the others own their own file. */
|
|
43
|
+
function targetKind(provider) {
|
|
44
|
+
return provider === 'codex' ? 'managed-block' : 'owned-file';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Detects whether a provider already has a footprint at this scope, so the
|
|
48
|
+
* wizard can pre-select the tools a developer actually uses. Looks for the
|
|
49
|
+
* provider's instruction directory/file rather than the MCP client config —
|
|
50
|
+
* this is about where agent instructions live, which the MCP detector does not
|
|
51
|
+
* model.
|
|
52
|
+
*/
|
|
53
|
+
export function detectProvider(provider, scope, cwd, home) {
|
|
54
|
+
switch (provider) {
|
|
55
|
+
case 'claude':
|
|
56
|
+
return existsSync(join(scope === 'project' ? cwd : home, '.claude'));
|
|
57
|
+
case 'cursor':
|
|
58
|
+
return existsSync(join(cwd, '.cursor'));
|
|
59
|
+
case 'codex':
|
|
60
|
+
return scope === 'project'
|
|
61
|
+
? existsSync(join(cwd, 'AGENTS.md'))
|
|
62
|
+
: existsSync(join(home, '.codex'));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the concrete write targets for the chosen providers × layers at a
|
|
67
|
+
* scope. Cursor is coerced to project scope (it has no global rules), so it is
|
|
68
|
+
* never silently dropped when a user picks "global".
|
|
69
|
+
*
|
|
70
|
+
* @param providers Selected providers.
|
|
71
|
+
* @param layers Selected skill layers.
|
|
72
|
+
* @param scope Requested scope.
|
|
73
|
+
* @param cwd Project root (defaults to `process.cwd()`).
|
|
74
|
+
* @param home User home (defaults to `os.homedir()`).
|
|
75
|
+
* @returns One {@link SkillTarget} per provider/layer.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveTargets(providers, layers, scope, cwd = process.cwd(), home = homedir()) {
|
|
78
|
+
const targets = [];
|
|
79
|
+
for (const provider of providers) {
|
|
80
|
+
const effectiveScope = supportsScope(provider, scope) ? scope : 'project';
|
|
81
|
+
for (const layer of layers) {
|
|
82
|
+
targets.push({
|
|
83
|
+
provider,
|
|
84
|
+
scope: effectiveScope,
|
|
85
|
+
layer,
|
|
86
|
+
path: resolvePath(provider, layer, effectiveScope, cwd, home),
|
|
87
|
+
kind: targetKind(provider),
|
|
88
|
+
detected: detectProvider(provider, effectiveScope, cwd, home),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return targets;
|
|
93
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the agent-skills generator.
|
|
3
|
+
*
|
|
4
|
+
* The skills feature renders one canonical, provider-neutral document per layer
|
|
5
|
+
* ({@link SkillLayer}) into each AI tool's native instruction format
|
|
6
|
+
* ({@link SkillProvider}) and installs it into a project or the user's home
|
|
7
|
+
* ({@link SkillScope}). These types model that pipeline: authored content
|
|
8
|
+
* ({@link SkillDoc}) + a live registry snapshot ({@link RegistrySnapshot}) →
|
|
9
|
+
* resolved write locations ({@link SkillTarget}) → idempotent write plans
|
|
10
|
+
* ({@link WritePlan}).
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -12,24 +12,31 @@ import { otherEntries, claudeEntries } from './other.js';
|
|
|
12
12
|
import { developerEntries } from './developer.js';
|
|
13
13
|
import { tradingEntries } from './trading.js';
|
|
14
14
|
/**
|
|
15
|
-
* Registered tool
|
|
16
|
-
* `fetch`) are registered ahead of the eBay API tools, matching the
|
|
15
|
+
* Registered tool families in registry execution order. Connector tools
|
|
16
|
+
* (`search`/`fetch`) are registered ahead of the eBay API tools, matching the
|
|
17
|
+
* prior registry. {@link registeredEntries} is derived from this list, so adding
|
|
18
|
+
* a family here both registers its tools and surfaces it in the family index.
|
|
17
19
|
*/
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
export const toolCategories = [
|
|
21
|
+
{ key: 'connector', title: 'Connector', entries: connectorEntries },
|
|
22
|
+
{ key: 'token-management', title: 'Token Management', entries: tokenManagementEntries },
|
|
23
|
+
{ key: 'account', title: 'Account', entries: accountEntries },
|
|
24
|
+
{ key: 'inventory', title: 'Inventory', entries: inventoryEntries },
|
|
25
|
+
{ key: 'fulfillment', title: 'Fulfillment', entries: fulfillmentEntries },
|
|
26
|
+
{ key: 'marketing', title: 'Marketing', entries: marketingEntries },
|
|
27
|
+
{ key: 'analytics', title: 'Analytics', entries: analyticsEntries },
|
|
28
|
+
{ key: 'metadata', title: 'Metadata', entries: metadataEntries },
|
|
29
|
+
{ key: 'taxonomy', title: 'Taxonomy', entries: taxonomyEntries },
|
|
30
|
+
{ key: 'communication', title: 'Communication', entries: communicationEntries },
|
|
31
|
+
{ key: 'other', title: 'Other', entries: otherEntries },
|
|
32
|
+
{ key: 'developer', title: 'Developer', entries: developerEntries },
|
|
33
|
+
{ key: 'trading', title: 'Trading', entries: tradingEntries },
|
|
32
34
|
];
|
|
35
|
+
/**
|
|
36
|
+
* Registered tool entries in registry execution order, flattened from
|
|
37
|
+
* {@link toolCategories}.
|
|
38
|
+
*/
|
|
39
|
+
export const registeredEntries = toolCategories.flatMap((category) => category.entries);
|
|
33
40
|
/**
|
|
34
41
|
* Handler-only entries: callable via `executeTool`/`getToolHandler` but intentionally
|
|
35
42
|
* not advertised to MCP clients (no registered definition). Preserves prior behavior
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
/**
|
|
3
|
+
* Shared terminal styling for the project's interactive CLIs.
|
|
4
|
+
*
|
|
5
|
+
* These helpers print to stdout, which is correct for the setup/skills wizards
|
|
6
|
+
* (the stdout-is-reserved rule applies to the MCP *server* runtime, not CLI
|
|
7
|
+
* tooling). The eBay brand palette matches the setup wizard.
|
|
8
|
+
*/
|
|
9
|
+
export const ebayPalette = {
|
|
10
|
+
red: chalk.hex('#E53238'),
|
|
11
|
+
blue: chalk.hex('#0064D2'),
|
|
12
|
+
yellow: chalk.hex('#F5AF02'),
|
|
13
|
+
green: chalk.hex('#86B817'),
|
|
14
|
+
};
|
|
15
|
+
/** Semantic text styles used across CLI output. */
|
|
16
|
+
export const ui = {
|
|
17
|
+
dim: chalk.dim,
|
|
18
|
+
bold: chalk.bold,
|
|
19
|
+
success: chalk.green,
|
|
20
|
+
warning: chalk.yellow,
|
|
21
|
+
error: chalk.red,
|
|
22
|
+
info: chalk.cyan,
|
|
23
|
+
};
|
|
24
|
+
/** Prints a green success line. */
|
|
25
|
+
export function printSuccess(message) {
|
|
26
|
+
console.log(` ${ui.success('✓')} ${message}`);
|
|
27
|
+
}
|
|
28
|
+
/** Prints a yellow warning line. */
|
|
29
|
+
export function printWarning(message) {
|
|
30
|
+
console.log(` ${ui.warning('⚠')} ${message}`);
|
|
31
|
+
}
|
|
32
|
+
/** Prints a red error line. */
|
|
33
|
+
export function printError(message) {
|
|
34
|
+
console.log(` ${ui.error('✗')} ${message}`);
|
|
35
|
+
}
|
|
36
|
+
/** Prints a cyan info line. */
|
|
37
|
+
export function printInfo(message) {
|
|
38
|
+
console.log(` ${ui.info('ℹ')} ${message}`);
|
|
39
|
+
}
|
|
40
|
+
/** Prints a bold section heading preceded by a blank line. */
|
|
41
|
+
export function printHeading(title) {
|
|
42
|
+
console.log(`\n ${ui.bold(title)}`);
|
|
43
|
+
}
|
|
44
|
+
/** Prints a dim horizontal rule. */
|
|
45
|
+
export function printRule(width = 56) {
|
|
46
|
+
console.log(' ' + ui.dim('─'.repeat(width)));
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebay-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server that exposes 100% of eBay's Sell APIs (332 tools across 270 endpoints) to AI assistants like Claude, Cursor, and Cline, covering inventory, order fulfillment, marketing, analytics, and developer tools with OAuth authentication and refresh-token support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"build/mcp/**/*.js",
|
|
33
33
|
"build/schemas/**/*.js",
|
|
34
34
|
"build/scripts/**/*.js",
|
|
35
|
+
"build/skills/**/*.js",
|
|
35
36
|
"build/tools/**/*.js",
|
|
36
37
|
"build/utils/**/*.js",
|
|
37
38
|
"build/types/index.js",
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
"test": "vitest run",
|
|
107
108
|
"test:coverage": "vitest run --coverage",
|
|
108
109
|
"setup": "tsx src/scripts/setup.ts",
|
|
110
|
+
"skills": "tsx src/scripts/skills.ts",
|
|
109
111
|
"sync": "tsx src/scripts/dev-sync.ts",
|
|
110
112
|
"diagnose": "tsx src/scripts/diagnostics.ts",
|
|
111
113
|
"typecheck": "tsc --noEmit",
|