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.
- package/README.de.md +27 -0
- package/README.es.md +27 -0
- package/README.fr.md +27 -0
- package/README.ja.md +27 -0
- package/README.ko.md +27 -0
- package/README.md +40 -0
- package/README.pt-BR.md +27 -0
- package/README.ru.md +27 -0
- package/README.zh-CN.md +27 -0
- package/build/config/environment.js +6 -0
- package/build/config/tool-families.js +79 -0
- package/build/mcp/runtime.js +51 -7
- package/build/mcp/tool-gating.js +198 -0
- package/build/mcp/ui-bridge.js +151 -0
- package/build/scripts/build-ui.js +44 -0
- package/build/tools/categories/analytics.js +4 -0
- package/build/tools/categories/fulfillment.js +6 -0
- package/build/tools/categories/inventory.js +6 -0
- package/build/tools/define-tool.js +27 -2
- package/build/tools/ui/archetypes.js +11 -0
- package/build/tools/ui/map-helpers.js +97 -0
- package/build/tools/ui/maps.js +419 -0
- package/build/tools/ui/view-models.js +20 -0
- package/build/ui/card.html +87 -0
- package/build/ui/chart.html +87 -0
- package/build/ui/table.html +87 -0
- package/package.json +14 -5
package/build/mcp/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
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 {
|
|
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 {
|
|
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
|
+
};
|