ebay-mcp 1.11.1 → 1.13.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.
@@ -1,6 +1,9 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { EbaySellerApi } from '../api/index.js';
3
3
  import { getEbayConfig, mcpConfig } from '../config/environment.js';
4
+ import { resolveToolGatingMode } from '../config/tool-families.js';
5
+ import { createToolGatingController, DYNAMIC_MODE_INSTRUCTIONS, registerMetaTools, toolNamesInFamilies, } from '../mcp/tool-gating.js';
6
+ import { buildUiToolResult, createUiBridge } from '../mcp/ui-bridge.js';
4
7
  import { getToolEntries } from '../tools/registry.js';
5
8
  import { getErrorMessage } from '../utils/errors.js';
6
9
  import { serverLogger, toolLogger } from '../utils/logger.js';
@@ -26,9 +29,11 @@ function formatToolFailure(error) {
26
29
  isError: true,
27
30
  };
28
31
  }
29
- function registerTool(server, api, entry, logToolExecution) {
32
+ function registerTool(server, api, entry, logToolExecution, ui) {
30
33
  const { definition, handler } = entry;
31
- server.registerTool(definition.name, {
34
+ // Registered plainly (no UI `_meta`) so every host gets a working text tool by
35
+ // default; the capability gate later flips `_meta.ui` on for UI-capable clients.
36
+ const registered = server.registerTool(definition.name, {
32
37
  description: definition.description,
33
38
  inputSchema: definition.inputSchema,
34
39
  }, async (args) => {
@@ -40,7 +45,9 @@ function registerTool(server, api, entry, logToolExecution) {
40
45
  if (logToolExecution) {
41
46
  toolLogger.debug(`Tool ${definition.name} completed successfully`);
42
47
  }
43
- return formatToolSuccess(result);
48
+ return ui.shouldRender(entry)
49
+ ? buildUiToolResult(entry.ui, result)
50
+ : formatToolSuccess(result);
44
51
  }
45
52
  catch (error) {
46
53
  const errorMessage = getErrorMessage(error);
@@ -50,18 +57,55 @@ function registerTool(server, api, entry, logToolExecution) {
50
57
  return formatToolFailure(error);
51
58
  }
52
59
  });
60
+ ui.register(entry, registered);
61
+ return registered;
53
62
  }
54
63
  /**
55
64
  * Create an MCP server runtime and register all eBay tool handlers.
56
65
  */
57
66
  export function createEbayMcpRuntime(options = {}) {
58
67
  const api = options.api ?? new EbaySellerApi(getEbayConfig());
59
- const server = new McpServer(options.serverConfig ?? mcpConfig);
60
- const entries = getToolEntries();
61
- serverLogger.info(`Registering ${entries.length} tools`);
68
+ const serverInfo = options.serverConfig ?? mcpConfig;
69
+ const mode = resolveToolGatingMode();
70
+ // Instructions are set only in dynamic mode so the agent knows the catalogue is
71
+ // hidden behind the discovery tools; default/static modes keep the handshake
72
+ // byte-for-byte unchanged (a bare single-arg construction).
73
+ const server = mode.kind === 'dynamic'
74
+ ? new McpServer(serverInfo, { instructions: DYNAMIC_MODE_INSTRUCTIONS })
75
+ : new McpServer(serverInfo);
76
+ const ui = createUiBridge(server, import.meta.url);
77
+ // Static mode registers only the named families; all and dynamic register the
78
+ // full catalogue (dynamic then disables it below, before the transport connects).
79
+ const allEntries = getToolEntries();
80
+ const entries = mode.kind === 'static'
81
+ ? (() => {
82
+ const names = toolNamesInFamilies(mode.families);
83
+ return allEntries.filter((entry) => names.has(entry.definition.name));
84
+ })()
85
+ : allEntries;
86
+ const handles = new Map();
62
87
  for (const entry of entries) {
63
- registerTool(server, api, entry, options.logToolExecution ?? false);
88
+ handles.set(entry.definition.name, registerTool(server, api, entry, options.logToolExecution ?? false, ui));
64
89
  }
90
+ if (mode.kind === 'dynamic') {
91
+ // Disable before `connect`: the SDK only emits `tools/listChanged` once the
92
+ // transport is connected, so these flips are silent. The agent re-enables the
93
+ // tools it needs via the meta-tools, which fire `listChanged` post-connect.
94
+ for (const handle of handles.values()) {
95
+ handle.disable();
96
+ }
97
+ registerMetaTools(server, createToolGatingController(handles));
98
+ serverLogger.info(`Dynamic tool mode: ${handles.size} eBay tools hidden behind 3 discovery tools`);
99
+ }
100
+ else if (mode.kind === 'static') {
101
+ serverLogger.info(`Static tool mode: registered ${handles.size} tools from families: ${mode.families.join(', ')}`);
102
+ }
103
+ else {
104
+ serverLogger.info(`Registering ${handles.size} tools`);
105
+ }
106
+ // Install after registration so every UI-eligible tool is captured before the
107
+ // gate can flip their metadata on a UI-capable client's `initialize`.
108
+ ui.installCapabilityGate();
65
109
  return {
66
110
  api,
67
111
  server,
@@ -0,0 +1,198 @@
1
+ import { z } from 'zod';
2
+ import { TOOL_FAMILY_KEYS } from '../config/tool-families.js';
3
+ import { toolCategories } from '../tools/categories/index.js';
4
+ /**
5
+ * Tool gating: the runtime half of the `EBAY_MCP_TOOLS=dynamic` feature.
6
+ *
7
+ * In dynamic mode the server registers every eBay tool but boots with them
8
+ * disabled, advertising only the three discovery meta-tools defined here. The
9
+ * agent browses the catalogue and enables just the tools it needs; the MCP SDK
10
+ * emits `tools/listChanged` automatically when a tool's `enabled` flag flips, so
11
+ * the host re-fetches and the freshly enabled tools appear natively, with their
12
+ * real input schemas and validation. This keeps advertised context proportional
13
+ * to what the agent actually uses instead of the full ~187-tool catalogue.
14
+ *
15
+ * The pure env parsing/validation lives in `@/config/tool-families.ts`; this
16
+ * module owns everything that needs the live tool registry.
17
+ */
18
+ /** Default page size for {@link ToolGatingController.list} when `limit` is omitted. */
19
+ const DEFAULT_LIST_LIMIT = 25;
20
+ /** Upper bound on a single discovery page, mirrored by the `list_ebay_tools` schema. */
21
+ const MAX_LIST_LIMIT = 100;
22
+ /**
23
+ * Initialize-time guidance shown to the agent in dynamic mode so it knows the
24
+ * catalogue is hidden behind the discovery tools. Set only when dynamic mode is
25
+ * active; the default and static modes leave the handshake untouched.
26
+ */
27
+ export const DYNAMIC_MODE_INSTRUCTIONS = 'This eBay server keeps its full tool catalogue hidden to save context. Only the discovery tools are visible right now. ' +
28
+ 'Call `list_ebay_tools` (no arguments) to see the tool families, then `list_ebay_tools` with a `family` or `query` to find specific tools, ' +
29
+ 'then `enable_ebay_tools` with the tool names you need — they will appear as normal tools you can call. ' +
30
+ 'Call `disable_ebay_tools` to remove tools you no longer need and reclaim context.';
31
+ /** Condenses a tool description to a single sentence for the discovery listing. */
32
+ function toSummary(description) {
33
+ const firstLine = description.split('\n', 1)[0].trim();
34
+ const sentenceEnd = firstLine.indexOf('. ');
35
+ const sentence = sentenceEnd === -1 ? firstLine : firstLine.slice(0, sentenceEnd + 1);
36
+ return sentence.length > 160 ? `${sentence.slice(0, 157)}...` : sentence;
37
+ }
38
+ /** Builds the discovery catalogue (tool rows, family overview, name→family index). */
39
+ function buildCatalog() {
40
+ const rows = [];
41
+ const families = [];
42
+ const familyByToolName = new Map();
43
+ for (const category of toolCategories) {
44
+ families.push({ key: category.key, title: category.title, count: category.entries.length });
45
+ for (const entry of category.entries) {
46
+ const { name, description } = entry.definition;
47
+ rows.push({ name, family: category.key, summary: toSummary(description) });
48
+ familyByToolName.set(name, category.key);
49
+ }
50
+ }
51
+ return { rows, families, familyByToolName };
52
+ }
53
+ const CATALOG = buildCatalog();
54
+ /**
55
+ * Resolves the tool names belonging to the given families, preserving the caller's
56
+ * ability to filter registry entries for static mode. Unknown family keys
57
+ * contribute nothing (they are rejected earlier by env validation).
58
+ */
59
+ export function toolNamesInFamilies(families) {
60
+ const wanted = new Set(families);
61
+ const names = new Set();
62
+ for (const [name, family] of CATALOG.familyByToolName) {
63
+ if (wanted.has(family)) {
64
+ names.add(name);
65
+ }
66
+ }
67
+ return names;
68
+ }
69
+ /** Parses an opaque list cursor (a stringified offset) back into a non-negative offset. */
70
+ function parseCursor(cursor) {
71
+ if (!cursor) {
72
+ return 0;
73
+ }
74
+ const offset = Number.parseInt(cursor, 10);
75
+ return Number.isInteger(offset) && offset > 0 ? offset : 0;
76
+ }
77
+ /**
78
+ * Creates the controller backing the discovery meta-tools.
79
+ *
80
+ * @param handles - Map of eBay tool name → its registered handle. Only these tools
81
+ * are gateable; the meta-tools themselves are intentionally absent so they can
82
+ * never be disabled and are excluded from `activeCount`.
83
+ */
84
+ export function createToolGatingController(handles) {
85
+ const activeCount = () => [...handles.values()].filter((handle) => handle.enabled).length;
86
+ const mutate = (names, enable) => {
87
+ const changed = [];
88
+ const unknown = [];
89
+ for (const name of names) {
90
+ const handle = handles.get(name);
91
+ if (!handle) {
92
+ unknown.push(name);
93
+ continue;
94
+ }
95
+ if (handle.enabled !== enable) {
96
+ if (enable) {
97
+ handle.enable();
98
+ }
99
+ else {
100
+ handle.disable();
101
+ }
102
+ }
103
+ changed.push(name);
104
+ }
105
+ return enable
106
+ ? { enabled: changed, unknown, activeCount: activeCount() }
107
+ : { disabled: changed, unknown, activeCount: activeCount() };
108
+ };
109
+ return {
110
+ list({ family, query, cursor, limit }) {
111
+ if (!family && !query) {
112
+ return {
113
+ families: CATALOG.families.map((row) => ({
114
+ ...row,
115
+ enabledCount: [...handles.entries()].filter(([name, handle]) => CATALOG.familyByToolName.get(name) === row.key && handle.enabled).length,
116
+ })),
117
+ hint: 'Call list_ebay_tools with { family } or { query } to list tools, then enable_ebay_tools.',
118
+ };
119
+ }
120
+ if (family && !CATALOG.families.some((row) => row.key === family)) {
121
+ return {
122
+ error: `Unknown tool family "${family}".`,
123
+ validFamilies: TOOL_FAMILY_KEYS,
124
+ };
125
+ }
126
+ const needle = query?.toLowerCase();
127
+ const matches = CATALOG.rows.filter((row) => {
128
+ if (family && row.family !== family) {
129
+ return false;
130
+ }
131
+ if (needle) {
132
+ return (row.name.toLowerCase().includes(needle) ||
133
+ row.summary.toLowerCase().includes(needle) ||
134
+ row.family.includes(needle));
135
+ }
136
+ return true;
137
+ });
138
+ const offset = parseCursor(cursor);
139
+ const size = Math.min(limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
140
+ const page = matches.slice(offset, offset + size);
141
+ const nextOffset = offset + size;
142
+ return {
143
+ tools: page.map((row) => ({ ...row, enabled: handles.get(row.name)?.enabled ?? false })),
144
+ total: matches.length,
145
+ nextCursor: nextOffset < matches.length ? String(nextOffset) : undefined,
146
+ };
147
+ },
148
+ enable: (names) => mutate(names, true),
149
+ disable: (names) => mutate(names, false),
150
+ };
151
+ }
152
+ /** Wraps controller output as a standard text tool result. */
153
+ function toToolResult(data) {
154
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
155
+ }
156
+ /**
157
+ * Registers the three discovery meta-tools on the server. Called only in dynamic
158
+ * mode, after the eBay tools are registered and disabled, so the controller sees
159
+ * every gateable handle.
160
+ */
161
+ export function registerMetaTools(server, controller) {
162
+ server.registerTool('list_ebay_tools', {
163
+ description: "Discover eBay tools without loading them into context. Call with no arguments to list the tool families; pass `family` to list a family's tools, or `query` to keyword-search across all tools. Returns lightweight rows (name, family, summary) — enable a tool with enable_ebay_tools to get its full schema and call it.",
164
+ inputSchema: {
165
+ family: z
166
+ .string()
167
+ .optional()
168
+ .describe('Restrict results to one family key (e.g. "inventory").'),
169
+ query: z
170
+ .string()
171
+ .optional()
172
+ .describe('Case-insensitive keyword matched against tool name, summary, and family.'),
173
+ cursor: z.string().optional().describe('Pagination cursor from a prior call.'),
174
+ limit: z
175
+ .number()
176
+ .int()
177
+ .positive()
178
+ .max(MAX_LIST_LIMIT)
179
+ .optional()
180
+ .describe(`Max rows per page (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}).`),
181
+ },
182
+ }, (args) => toToolResult(controller.list(args)));
183
+ server.registerTool('enable_ebay_tools', {
184
+ description: 'Load specific eBay tools into context so they can be called. Pass the exact tool names from list_ebay_tools. Returns the names enabled, any unknown names, and the active tool count.',
185
+ inputSchema: {
186
+ names: z
187
+ .array(z.string())
188
+ .min(1)
189
+ .describe('Exact tool names to enable, as returned by list_ebay_tools.'),
190
+ },
191
+ }, ({ names }) => toToolResult(controller.enable(names)));
192
+ server.registerTool('disable_ebay_tools', {
193
+ description: 'Unload eBay tools previously enabled, reclaiming their context. Pass the exact tool names. Returns the names disabled, any unknown names, and the active tool count.',
194
+ inputSchema: {
195
+ names: z.array(z.string()).min(1).describe('Exact tool names to disable.'),
196
+ },
197
+ }, ({ names }) => toToolResult(controller.disable(names)));
198
+ }
@@ -0,0 +1,151 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getUiCapability, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, } from '@modelcontextprotocol/ext-apps/server';
5
+ import { uiArchetypes } from '../tools/ui/archetypes.js';
6
+ import { serverLogger } from '../utils/logger.js';
7
+ /**
8
+ * Decides whether interactive views should be advertised to the connected client.
9
+ *
10
+ * Pure and injectable (capabilities + env) so the gate is unit-testable without a
11
+ * live transport. Two independent conditions must both hold: the operator has not
12
+ * disabled UI via `EBAY_MCP_UI=off`, and the client advertised support for the MCP
13
+ * Apps HTML profile.
14
+ */
15
+ export function resolveUiEnabled(capabilities, env = process.env) {
16
+ if (env.EBAY_MCP_UI === 'off') {
17
+ return false;
18
+ }
19
+ const capability = getUiCapability(capabilities);
20
+ return Boolean(capability?.mimeTypes?.includes(RESOURCE_MIME_TYPE));
21
+ }
22
+ /**
23
+ * Builds the one-line text fallback that accompanies a rendered view.
24
+ *
25
+ * Hosts that render the app show this line in the transcript; the model reads it
26
+ * plus the structured `structuredContent`. Keeping it a short summary (rather than
27
+ * re-dumping the full JSON the view already carries) is what keeps the UI path
28
+ * from costing more tokens than the plain path.
29
+ */
30
+ export function summarizeView(view) {
31
+ switch (view.archetype) {
32
+ case 'table': {
33
+ const count = view.rows.length;
34
+ const tail = view.footnote ? ` — ${view.footnote}` : '';
35
+ return `${view.title ?? 'Results'}: ${count} ${count === 1 ? 'row' : 'rows'}${tail}. Rendered as an interactive table.`;
36
+ }
37
+ case 'card': {
38
+ const subtitle = view.subtitle ? ` — ${view.subtitle}` : '';
39
+ const fieldCount = view.sections.reduce((total, section) => total + section.fields.length, 0);
40
+ return `${view.title ?? 'Details'}${subtitle}: ${fieldCount} ${fieldCount === 1 ? 'field' : 'fields'}. Rendered as an interactive detail card.`;
41
+ }
42
+ case 'chart': {
43
+ const pointCount = view.series.reduce((total, series) => total + series.points.length, 0);
44
+ return `${view.title ?? 'Chart'} (${view.kind}): ${view.series.length} series, ${pointCount} ${pointCount === 1 ? 'point' : 'points'}. Rendered as an interactive chart.`;
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Adapts a {@link ViewModel} to the SDK's open-record `structuredContent` type.
50
+ *
51
+ * A view model is a string-keyed JSON object by construction, but TypeScript does
52
+ * not give `interface` types an implicit string index signature, so it refuses the
53
+ * direct cast. The `unknown` hop is the standard, safe bridge across that gap —
54
+ * preferable to polluting every view-model interface with `[key: string]: unknown`,
55
+ * which would silently disable excess-property checks in the tool mappers.
56
+ */
57
+ function asStructuredContent(view) {
58
+ return view;
59
+ }
60
+ /**
61
+ * Projects a tool result into its view and packages it as a UI tool result: a
62
+ * compact summary line, the view model as `structuredContent`, and the
63
+ * `_meta.ui.resourceUri` that tells the host which app to render.
64
+ */
65
+ export function buildUiToolResult(ui, result) {
66
+ const view = ui.map(result);
67
+ return {
68
+ content: [{ type: 'text', text: summarizeView(view) }],
69
+ structuredContent: asStructuredContent(view),
70
+ _meta: { ui: { resourceUri: ui.resourceUri } },
71
+ };
72
+ }
73
+ /**
74
+ * Walks up from `startDir` to the nearest directory containing a `package.json`.
75
+ * Used to locate the repo root so built views resolve identically whether the
76
+ * server runs from `build/` (prod) or `src/` via tsx (dev).
77
+ */
78
+ export function findRepoRoot(startDir) {
79
+ let current = startDir;
80
+ for (;;) {
81
+ if (existsSync(join(current, 'package.json'))) {
82
+ return current;
83
+ }
84
+ const parent = dirname(current);
85
+ if (parent === current) {
86
+ return undefined;
87
+ }
88
+ current = parent;
89
+ }
90
+ }
91
+ /** Resolves the directory that holds the built archetype HTML files (`build/ui/`). */
92
+ export function resolveBuiltUiDir(moduleUrl) {
93
+ const fromDir = dirname(fileURLToPath(moduleUrl));
94
+ const root = findRepoRoot(fromDir);
95
+ return root ? join(root, 'build', 'ui') : join(fromDir, '..', 'ui');
96
+ }
97
+ /**
98
+ * Constructs the {@link UiBridge}: reads the built archetype HTML, registers each
99
+ * found view as a `ui://` resource, and returns the runtime-facing facade.
100
+ *
101
+ * @param server - The MCP server the resources and gate attach to.
102
+ * @param moduleUrl - `import.meta.url` of the caller, used to locate `build/ui/`.
103
+ */
104
+ export function createUiBridge(server, moduleUrl) {
105
+ const builtUiDir = resolveBuiltUiDir(moduleUrl);
106
+ const available = new Set();
107
+ // `Object.keys` widens to `string[]`; the cast restores the manifest's key type.
108
+ for (const archetype of Object.keys(uiArchetypes)) {
109
+ const { uri, name, htmlFile } = uiArchetypes[archetype];
110
+ const htmlPath = join(builtUiDir, htmlFile);
111
+ if (!existsSync(htmlPath)) {
112
+ serverLogger.warn(`UI view not built; "${uri}" disabled (expected ${htmlPath})`);
113
+ continue;
114
+ }
115
+ const html = readFileSync(htmlPath, 'utf-8');
116
+ registerAppResource(server, name, uri, {}, () => ({
117
+ contents: [{ uri, mimeType: RESOURCE_MIME_TYPE, text: html }],
118
+ }));
119
+ available.add(archetype);
120
+ }
121
+ let uiEnabled = false;
122
+ const uiTools = [];
123
+ return {
124
+ register(entry, registered) {
125
+ if (entry.ui && available.has(entry.ui.archetype)) {
126
+ uiTools.push({ registered, resourceUri: entry.ui.resourceUri });
127
+ }
128
+ },
129
+ shouldRender(entry) {
130
+ return uiEnabled && entry.ui != null && available.has(entry.ui.archetype);
131
+ },
132
+ installCapabilityGate() {
133
+ const priorOnInitialized = server.server.oninitialized;
134
+ server.server.oninitialized = () => {
135
+ priorOnInitialized?.();
136
+ uiEnabled = resolveUiEnabled(server.server.getClientCapabilities());
137
+ if (!uiEnabled) {
138
+ return;
139
+ }
140
+ for (const { registered, resourceUri } of uiTools) {
141
+ registered.update({
142
+ _meta: {
143
+ ui: { resourceUri },
144
+ [RESOURCE_URI_META_KEY]: resourceUri,
145
+ },
146
+ });
147
+ }
148
+ };
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Builds the three interactive MCP Apps views into self-contained HTML files
3
+ * under `build/ui/`.
4
+ *
5
+ * Why a script rather than a single `vite.config.ts`: `vite-plugin-singlefile`
6
+ * inlines all dynamic imports, which Rollup only permits with a single input.
7
+ * The three archetypes are therefore built one at a time (each its own
8
+ * single-page build) into a shared output directory — the first pass clears it,
9
+ * the rest append. The result is exactly what `src/mcp/ui-bridge.ts` expects:
10
+ * `build/ui/{table,card,chart}.html`, each with its JS and CSS inlined so the
11
+ * server can serve it verbatim as a `ui://` resource with no sibling requests.
12
+ *
13
+ * Run via `npm run build:ui` (also chained into `npm run build`).
14
+ */
15
+ import { dirname, resolve } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { build } from 'vite';
18
+ import { viteSingleFile } from 'vite-plugin-singlefile';
19
+ import { uiArchetypes } from '../tools/ui/archetypes.js';
20
+ import { serverLogger } from '../utils/logger.js';
21
+ const scriptsDir = dirname(fileURLToPath(import.meta.url));
22
+ const repoRoot = resolve(scriptsDir, '../..');
23
+ const uiDir = resolve(repoRoot, 'ui');
24
+ const outDir = resolve(repoRoot, 'build', 'ui');
25
+ /** Vite config for a single archetype's single-page, fully-inlined build. */
26
+ function configFor(archetype, clean) {
27
+ return {
28
+ root: uiDir,
29
+ configFile: false,
30
+ logLevel: 'warn',
31
+ plugins: [viteSingleFile()],
32
+ esbuild: { jsx: 'automatic', jsxImportSource: 'react' },
33
+ build: {
34
+ outDir,
35
+ emptyOutDir: clean,
36
+ rollupOptions: { input: resolve(uiDir, uiArchetypes[archetype].htmlFile) },
37
+ },
38
+ };
39
+ }
40
+ const archetypes = Object.keys(uiArchetypes);
41
+ for (const [index, archetype] of archetypes.entries()) {
42
+ await build(configFor(archetype, index === 0));
43
+ serverLogger.info(`Built UI view "${uiArchetypes[archetype].htmlFile}"`);
44
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { defineTool } from '../../tools/define-tool.js';
3
+ import { mapCustomerServiceMetricToChart, mapStandardsProfileToCard, mapTrafficReportToChart, } from '../../tools/ui/maps.js';
3
4
  /** Analytics API tools for seller traffic and performance reporting. */
4
5
  export const analyticsEntries = [
5
6
  defineTool({
@@ -20,6 +21,7 @@ export const analyticsEntries = [
20
21
  description: 'Traffic report data',
21
22
  },
22
23
  handler: (api, args) => api.analytics.getTrafficReport(args.dimension, args.filter, args.metric, args.sort),
24
+ ui: { archetype: 'chart', map: mapTrafficReportToChart },
23
25
  }),
24
26
  defineTool({
25
27
  name: 'ebay_find_seller_standards_profiles',
@@ -51,6 +53,7 @@ export const analyticsEntries = [
51
53
  description: 'Seller standards profile data',
52
54
  },
53
55
  handler: (api, args) => api.analytics.getSellerStandardsProfile(args.program, args.cycle),
56
+ ui: { archetype: 'card', map: mapStandardsProfileToCard },
54
57
  }),
55
58
  defineTool({
56
59
  name: 'ebay_get_customer_service_metric',
@@ -68,5 +71,6 @@ export const analyticsEntries = [
68
71
  description: 'Customer service metric data',
69
72
  },
70
73
  handler: (api, args) => api.analytics.getCustomerServiceMetric(args.customerServiceMetricType, args.evaluationType, args.evaluationMarketplaceId),
74
+ ui: { archetype: 'chart', map: mapCustomerServiceMetricToChart },
71
75
  }),
72
76
  ];
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema';
3
3
  import { defineTool } from '../../tools/define-tool.js';
4
+ import { mapDisputeSummariesToTable, mapDisputeToCard, mapFulfillmentsToTable, mapOrdersToTable, mapOrderToCard, } from '../../tools/ui/maps.js';
4
5
  import { shippingFulfillmentSchema } from '../schemas.js';
5
6
  import { getOrdersOutputSchema, getOrderOutputSchema, createShippingFulfillmentOutputSchema, getShippingFulfillmentsOutputSchema, issueRefundOutputSchema, getPaymentDisputesOutputSchema, } from '../../schemas/fulfillment/orders.js';
6
7
  /** Fulfillment API tools for orders, shipping fulfillments, refunds, and cancellations. */
@@ -21,6 +22,7 @@ export const fulfillmentEntries = [
21
22
  $refStrategy: 'none',
22
23
  }),
23
24
  handler: (api, args) => api.fulfillment.getOrders(args.filter, args.limit, args.offset),
25
+ ui: { archetype: 'table', map: mapOrdersToTable },
24
26
  }),
25
27
  defineTool({
26
28
  name: 'ebay_get_order',
@@ -33,6 +35,7 @@ export const fulfillmentEntries = [
33
35
  $refStrategy: 'none',
34
36
  }),
35
37
  handler: (api, args) => api.fulfillment.getOrder(args.orderId),
38
+ ui: { archetype: 'card', map: mapOrderToCard },
36
39
  }),
37
40
  defineTool({
38
41
  name: 'ebay_create_shipping_fulfillment',
@@ -58,6 +61,7 @@ export const fulfillmentEntries = [
58
61
  $refStrategy: 'none',
59
62
  }),
60
63
  handler: (api, args) => api.fulfillment.getShippingFulfillments(args.orderId),
64
+ ui: { archetype: 'table', map: mapFulfillmentsToTable },
61
65
  }),
62
66
  defineTool({
63
67
  name: 'ebay_get_shipping_fulfillment',
@@ -157,6 +161,7 @@ export const fulfillmentEntries = [
157
161
  limit: args.limit,
158
162
  offset: args.offset,
159
163
  }),
164
+ ui: { archetype: 'table', map: mapDisputeSummariesToTable },
160
165
  }),
161
166
  defineTool({
162
167
  name: 'ebay_get_payment_dispute',
@@ -176,6 +181,7 @@ export const fulfillmentEntries = [
176
181
  description: 'Payment dispute details',
177
182
  },
178
183
  handler: (api, args) => api.dispute.getPaymentDispute(args.paymentDisputeId),
184
+ ui: { archetype: 'card', map: mapDisputeToCard },
179
185
  }),
180
186
  defineTool({
181
187
  name: 'ebay_get_payment_dispute_activities',
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema';
3
3
  import { MarketplaceId } from '../../types/ebay-enums.js';
4
4
  import { defineTool } from '../../tools/define-tool.js';
5
+ import { mapInventoryItemsToTable, mapInventoryItemToCard, mapLocationsToTable, mapOffersToTable, mapOfferToCard, } from '../../tools/ui/maps.js';
5
6
  import { bulkInventoryItemRequestSchema, bulkMigrateRequestSchema, bulkOfferRequestSchema, bulkPriceQuantityRequestSchema, bulkPublishRequestSchema, inventoryItemGroupSchema, inventoryItemSchema, listingFeesRequestSchema, locationSchema, offerSchema, productCompatibilitySchema, } from '../schemas.js';
6
7
  import { getInventoryItemsOutputSchema, getInventoryItemOutputSchema, createInventoryItemOutputSchema, getOffersOutputSchema, createOfferOutputSchema, publishOfferOutputSchema, offerResponseSchema, getInventoryLocationsOutputSchema, createInventoryLocationOutputSchema, getProductCompatibilityOutputSchema, getInventoryItemGroupOutputSchema, bulkInventoryItemResponseSchema, bulkOfferResponseSchema, bulkPublishResponseSchema, } from '../../schemas/inventory-management/inventory.js';
7
8
  /** Inventory API tools for seller inventory items, offers, locations, and bulk operations. */
@@ -18,6 +19,7 @@ export const inventoryEntries = [
18
19
  $refStrategy: 'none',
19
20
  }),
20
21
  handler: (api, args) => api.inventory.getInventoryItems(args.limit, args.offset),
22
+ ui: { archetype: 'table', map: mapInventoryItemsToTable },
21
23
  }),
22
24
  defineTool({
23
25
  name: 'ebay_get_inventory_item',
@@ -30,6 +32,7 @@ export const inventoryEntries = [
30
32
  $refStrategy: 'none',
31
33
  }),
32
34
  handler: (api, args) => api.inventory.getInventoryItem(args.sku),
35
+ ui: { archetype: 'card', map: mapInventoryItemToCard },
33
36
  }),
34
37
  defineTool({
35
38
  name: 'ebay_create_inventory_item',
@@ -70,6 +73,7 @@ export const inventoryEntries = [
70
73
  $refStrategy: 'none',
71
74
  }),
72
75
  handler: (api, args) => api.inventory.getOffers(args.sku, args.marketplaceId, args.limit),
76
+ ui: { archetype: 'table', map: mapOffersToTable },
73
77
  }),
74
78
  defineTool({
75
79
  name: 'ebay_create_offer',
@@ -232,6 +236,7 @@ export const inventoryEntries = [
232
236
  $refStrategy: 'none',
233
237
  }),
234
238
  handler: (api, args) => api.inventory.getInventoryLocations(args.limit, args.offset),
239
+ ui: { archetype: 'table', map: mapLocationsToTable },
235
240
  }),
236
241
  defineTool({
237
242
  name: 'ebay_get_inventory_location',
@@ -322,6 +327,7 @@ export const inventoryEntries = [
322
327
  $refStrategy: 'none',
323
328
  }),
324
329
  handler: (api, args) => api.inventory.getOffer(args.offerId),
330
+ ui: { archetype: 'card', map: mapOfferToCard },
325
331
  }),
326
332
  defineTool({
327
333
  name: 'ebay_update_offer',
@@ -1,4 +1,21 @@
1
1
  import { z } from 'zod';
2
+ import { uiArchetypes } from '../tools/ui/archetypes.js';
3
+ /**
4
+ * Erases the compile-time result type from a {@link ToolUiSpec} so the binding can
5
+ * live on the non-generic {@link ToolEntry}, and resolves the archetype's
6
+ * `resourceUri` from the single-source-of-truth {@link uiArchetypes} manifest.
7
+ *
8
+ * The `result as Result` cast is the type-erasure boundary: at runtime `map` only
9
+ * ever receives the very value its own handler returned, so the cast restores the
10
+ * type the mapper was written and type-checked against.
11
+ */
12
+ function resolveToolUi(ui) {
13
+ return {
14
+ archetype: ui.archetype,
15
+ resourceUri: uiArchetypes[ui.archetype].uri,
16
+ map: (result) => ui.map(result),
17
+ };
18
+ }
2
19
  /**
3
20
  * Binds a tool's Zod input shape to its handler so the shape is the single
4
21
  * source of truth for both the advertised MCP schema and the handler's
@@ -38,7 +55,11 @@ export function defineTool(spec) {
38
55
  // the caller's `await`, and the handler's returned promise passes straight
39
56
  // through without an extra await layer.
40
57
  const handler = (api, args) => spec.handler(api, schema.parse(args));
41
- return { definition: toDefinition(spec), handler };
58
+ return {
59
+ definition: toDefinition(spec),
60
+ handler,
61
+ ui: spec.ui ? resolveToolUi(spec.ui) : undefined,
62
+ };
42
63
  }
43
64
  /**
44
65
  * Like {@link defineTool}, but invokes the handler with the raw transport
@@ -57,5 +78,9 @@ export function defineTool(spec) {
57
78
  */
58
79
  export function rawTool(spec) {
59
80
  const handler = (api, args) => spec.handler(api, args);
60
- return { definition: toDefinition(spec), handler };
81
+ return {
82
+ definition: toDefinition(spec),
83
+ handler,
84
+ ui: spec.ui ? resolveToolUi(spec.ui) : undefined,
85
+ };
61
86
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The archetype manifest — the authoritative map from {@link ViewArchetype} to its
3
+ * resource URI and built asset. Both `defineTool` (to resolve a tool's
4
+ * `resourceUri`) and the runtime seam (to register resources and gate emission)
5
+ * read from here, so the two halves can never disagree on a URI or filename.
6
+ */
7
+ export const uiArchetypes = {
8
+ table: { uri: 'ui://ebay/table.html', name: 'eBay table view', htmlFile: 'table.html' },
9
+ card: { uri: 'ui://ebay/card.html', name: 'eBay detail card view', htmlFile: 'card.html' },
10
+ chart: { uri: 'ui://ebay/chart.html', name: 'eBay chart view', htmlFile: 'chart.html' },
11
+ };