@pixelsandclicks_/mcp-server 0.0.0-stage → 1.0.1
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.md +76 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/data/articles.json +1822 -0
- package/dist/data/codes.json +1230 -0
- package/dist/data.d.ts +5 -0
- package/dist/data.js +131 -0
- package/dist/search.d.ts +8 -0
- package/dist/search.js +86 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +268 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +1 -0
- package/package.json +46 -4
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DeductionCode, Article } from "./types.js";
|
|
2
|
+
export declare function loadDeductionCodes(): DeductionCode[];
|
|
3
|
+
export declare function loadArticles(): Article[];
|
|
4
|
+
export declare function deductionCodeUrl(slug: string): string;
|
|
5
|
+
export declare function articleUrl(slug: string): string;
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
const SITE_URL = "https://pixelsandclicks.net";
|
|
5
|
+
function tryBundledJson(filename) {
|
|
6
|
+
const bundled = path.resolve(import.meta.dirname, "data", filename);
|
|
7
|
+
if (fs.existsSync(bundled)) {
|
|
8
|
+
return JSON.parse(fs.readFileSync(bundled, "utf-8"));
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function findContentDir() {
|
|
13
|
+
let dir = path.resolve(import.meta.dirname, "..");
|
|
14
|
+
for (let i = 0; i < 5; i++) {
|
|
15
|
+
const candidate = path.join(dir, "src", "content");
|
|
16
|
+
if (fs.existsSync(candidate))
|
|
17
|
+
return candidate;
|
|
18
|
+
dir = path.dirname(dir);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function slugFromFilename(filename) {
|
|
23
|
+
return path.basename(filename, path.extname(filename));
|
|
24
|
+
}
|
|
25
|
+
function loadCodesFromMarkdown(contentDir) {
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const matter = require("gray-matter");
|
|
28
|
+
const codesDir = path.join(contentDir, "deductionCodes");
|
|
29
|
+
if (!fs.existsSync(codesDir))
|
|
30
|
+
return [];
|
|
31
|
+
return fs.readdirSync(codesDir)
|
|
32
|
+
.filter(f => f.endsWith(".md") || f.endsWith(".mdx"))
|
|
33
|
+
.map(filename => {
|
|
34
|
+
const raw = fs.readFileSync(path.join(codesDir, filename), "utf-8");
|
|
35
|
+
const { data } = matter(raw);
|
|
36
|
+
const slug = slugFromFilename(filename);
|
|
37
|
+
return {
|
|
38
|
+
slug,
|
|
39
|
+
retailer: data.retailer,
|
|
40
|
+
code: data.code,
|
|
41
|
+
codeName: data.codeName,
|
|
42
|
+
title: data.title,
|
|
43
|
+
description: data.description,
|
|
44
|
+
draft: data.draft ?? false,
|
|
45
|
+
family: data.family,
|
|
46
|
+
penalty: data.penalty,
|
|
47
|
+
disputableStatus: data.disputableStatus,
|
|
48
|
+
answer: data.answer,
|
|
49
|
+
answerQualifier: data.answerQualifier,
|
|
50
|
+
meaning: data.meaning,
|
|
51
|
+
meaningPoints: data.meaningPoints,
|
|
52
|
+
rootCauses: data.rootCauses ?? [],
|
|
53
|
+
rootCauseCategories: data.rootCauseCategories,
|
|
54
|
+
evidenceRequired: data.evidenceRequired ?? [],
|
|
55
|
+
disputable: data.disputable,
|
|
56
|
+
disputeWindow: data.disputeWindow,
|
|
57
|
+
prevention: data.prevention ?? [],
|
|
58
|
+
erpPrevention: data.erpPrevention,
|
|
59
|
+
faq: data.faq ?? [],
|
|
60
|
+
};
|
|
61
|
+
})
|
|
62
|
+
.filter(c => !c.draft);
|
|
63
|
+
}
|
|
64
|
+
function loadArticlesFromMarkdown(contentDir) {
|
|
65
|
+
const require = createRequire(import.meta.url);
|
|
66
|
+
const matter = require("gray-matter");
|
|
67
|
+
const articlesDir = path.join(contentDir, "articles");
|
|
68
|
+
if (!fs.existsSync(articlesDir))
|
|
69
|
+
return [];
|
|
70
|
+
return fs.readdirSync(articlesDir)
|
|
71
|
+
.filter(f => f.endsWith(".md") || f.endsWith(".mdx"))
|
|
72
|
+
.map(filename => {
|
|
73
|
+
const raw = fs.readFileSync(path.join(articlesDir, filename), "utf-8");
|
|
74
|
+
const { data } = matter(raw);
|
|
75
|
+
const slug = slugFromFilename(filename);
|
|
76
|
+
return {
|
|
77
|
+
slug,
|
|
78
|
+
title: data.title,
|
|
79
|
+
description: data.description,
|
|
80
|
+
author: data.author ?? "Pixels and Clicks",
|
|
81
|
+
tags: data.tags ?? [],
|
|
82
|
+
cluster: data.cluster,
|
|
83
|
+
draft: data.draft ?? false,
|
|
84
|
+
answer: data.answer,
|
|
85
|
+
answerQualifier: data.answerQualifier,
|
|
86
|
+
faq: data.faq ?? [],
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
.filter(a => !a.draft);
|
|
90
|
+
}
|
|
91
|
+
export function loadDeductionCodes() {
|
|
92
|
+
const bundled = tryBundledJson("codes.json");
|
|
93
|
+
if (bundled) {
|
|
94
|
+
return bundled
|
|
95
|
+
.filter(c => !c.draft)
|
|
96
|
+
.map(c => ({
|
|
97
|
+
...c,
|
|
98
|
+
rootCauses: c.rootCauses ?? [],
|
|
99
|
+
evidenceRequired: c.evidenceRequired ?? [],
|
|
100
|
+
prevention: c.prevention ?? [],
|
|
101
|
+
faq: c.faq ?? [],
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
const contentDir = findContentDir();
|
|
105
|
+
if (!contentDir)
|
|
106
|
+
return [];
|
|
107
|
+
return loadCodesFromMarkdown(contentDir);
|
|
108
|
+
}
|
|
109
|
+
export function loadArticles() {
|
|
110
|
+
const bundled = tryBundledJson("articles.json");
|
|
111
|
+
if (bundled) {
|
|
112
|
+
return bundled
|
|
113
|
+
.filter(a => !a.draft)
|
|
114
|
+
.map(a => ({
|
|
115
|
+
...a,
|
|
116
|
+
author: a.author ?? "Pixels and Clicks",
|
|
117
|
+
tags: a.tags ?? [],
|
|
118
|
+
faq: a.faq ?? [],
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
const contentDir = findContentDir();
|
|
122
|
+
if (!contentDir)
|
|
123
|
+
return [];
|
|
124
|
+
return loadArticlesFromMarkdown(contentDir);
|
|
125
|
+
}
|
|
126
|
+
export function deductionCodeUrl(slug) {
|
|
127
|
+
return `${SITE_URL}/deduction-codes/${slug}`;
|
|
128
|
+
}
|
|
129
|
+
export function articleUrl(slug) {
|
|
130
|
+
return `${SITE_URL}/articles/${slug}`;
|
|
131
|
+
}
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DeductionCode, Article, SearchResult } from "./types.js";
|
|
2
|
+
export declare function searchDeductionCodes(codes: DeductionCode[], query: string, filters?: {
|
|
3
|
+
family?: string;
|
|
4
|
+
retailer?: string;
|
|
5
|
+
}): SearchResult[];
|
|
6
|
+
export declare function searchArticles(articles: Article[], query: string, filters?: {
|
|
7
|
+
cluster?: string;
|
|
8
|
+
}): SearchResult[];
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { deductionCodeUrl, articleUrl } from "./data.js";
|
|
2
|
+
function tokenize(text) {
|
|
3
|
+
return text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
4
|
+
}
|
|
5
|
+
function scoreMatch(text, queryTokens) {
|
|
6
|
+
const lower = text.toLowerCase();
|
|
7
|
+
let score = 0;
|
|
8
|
+
for (const token of queryTokens) {
|
|
9
|
+
if (lower.includes(token)) {
|
|
10
|
+
score += 1;
|
|
11
|
+
// Bonus for exact word boundary match
|
|
12
|
+
if (new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(lower)) {
|
|
13
|
+
score += 0.5;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return score;
|
|
18
|
+
}
|
|
19
|
+
export function searchDeductionCodes(codes, query, filters) {
|
|
20
|
+
const queryTokens = tokenize(query);
|
|
21
|
+
let filtered = codes;
|
|
22
|
+
if (filters?.family) {
|
|
23
|
+
filtered = filtered.filter(c => c.family?.toLowerCase() === filters.family.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
if (filters?.retailer) {
|
|
26
|
+
filtered = filtered.filter(c => c.retailer.toLowerCase() === filters.retailer.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
return filtered
|
|
29
|
+
.map(code => {
|
|
30
|
+
const searchableText = [
|
|
31
|
+
code.title,
|
|
32
|
+
code.codeName,
|
|
33
|
+
code.code,
|
|
34
|
+
code.description,
|
|
35
|
+
code.meaning,
|
|
36
|
+
code.answer ?? "",
|
|
37
|
+
code.family ?? "",
|
|
38
|
+
...(code.meaningPoints ?? []),
|
|
39
|
+
...(code.prevention),
|
|
40
|
+
].join(" ");
|
|
41
|
+
let relevance = scoreMatch(searchableText, queryTokens);
|
|
42
|
+
// Title and code-name matches are more likely what the user wants
|
|
43
|
+
relevance += scoreMatch(code.title, queryTokens) * 2;
|
|
44
|
+
relevance += scoreMatch(code.codeName, queryTokens) * 1.5;
|
|
45
|
+
return {
|
|
46
|
+
type: "deduction_code",
|
|
47
|
+
slug: code.slug,
|
|
48
|
+
title: code.title,
|
|
49
|
+
description: code.description,
|
|
50
|
+
relevance,
|
|
51
|
+
url: deductionCodeUrl(code.slug),
|
|
52
|
+
};
|
|
53
|
+
})
|
|
54
|
+
.filter(r => r.relevance > 0)
|
|
55
|
+
.sort((a, b) => b.relevance - a.relevance);
|
|
56
|
+
}
|
|
57
|
+
export function searchArticles(articles, query, filters) {
|
|
58
|
+
const queryTokens = tokenize(query);
|
|
59
|
+
let filtered = articles;
|
|
60
|
+
if (filters?.cluster) {
|
|
61
|
+
filtered = filtered.filter(a => a.cluster?.toLowerCase() === filters.cluster.toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
return filtered
|
|
64
|
+
.map(article => {
|
|
65
|
+
const searchableText = [
|
|
66
|
+
article.title,
|
|
67
|
+
article.description,
|
|
68
|
+
article.answer ?? "",
|
|
69
|
+
article.tags.join(" "),
|
|
70
|
+
...(article.faq.map(f => `${f.q} ${f.a}`)),
|
|
71
|
+
].join(" ");
|
|
72
|
+
let relevance = scoreMatch(searchableText, queryTokens);
|
|
73
|
+
// Title matches are more likely what the user wants
|
|
74
|
+
relevance += scoreMatch(article.title, queryTokens) * 2;
|
|
75
|
+
return {
|
|
76
|
+
type: "article",
|
|
77
|
+
slug: article.slug,
|
|
78
|
+
title: article.title,
|
|
79
|
+
description: article.description,
|
|
80
|
+
relevance,
|
|
81
|
+
url: articleUrl(article.slug),
|
|
82
|
+
};
|
|
83
|
+
})
|
|
84
|
+
.filter(r => r.relevance > 0)
|
|
85
|
+
.sort((a, b) => b.relevance - a.relevance);
|
|
86
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { loadDeductionCodes, loadArticles, deductionCodeUrl, articleUrl, } from "./data.js";
|
|
4
|
+
import { searchDeductionCodes, searchArticles } from "./search.js";
|
|
5
|
+
const SITE_URL = "https://pixelsandclicks.net";
|
|
6
|
+
export function createServer() {
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "pixels-and-clicks",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
});
|
|
11
|
+
const codes = loadDeductionCodes();
|
|
12
|
+
const articles = loadArticles();
|
|
13
|
+
// --- Tool 1: lookup_deduction_code ---
|
|
14
|
+
server.tool("lookup_deduction_code", "Look up a retail deduction code by retailer and code number. Returns meaning, root causes, evidence required, dispute path, prevention steps, and a link to the full reference on pixelsandclicks.net.", {
|
|
15
|
+
retailer: z.enum(["Walmart", "Target", "Amazon", "Kroger"]),
|
|
16
|
+
code: z.string().describe("Code number or identifier as it appears on the remittance, e.g. '22', 'ASN-ACC', 'SIFR'"),
|
|
17
|
+
}, async ({ retailer, code }) => {
|
|
18
|
+
const entry = codes.find((c) => c.retailer === retailer && c.code.toLowerCase() === code.toLowerCase());
|
|
19
|
+
if (!entry) {
|
|
20
|
+
const available = codes
|
|
21
|
+
.filter((c) => c.retailer === retailer)
|
|
22
|
+
.map((c) => `${c.code} (${c.codeName})`)
|
|
23
|
+
.join(", ");
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: `No entry found for ${retailer} code "${code}". Available ${retailer} codes: ${available || "none"}.\n\nBrowse all codes: ${SITE_URL}/deduction-codes`,
|
|
28
|
+
}],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
content: [{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: JSON.stringify({
|
|
35
|
+
retailer: entry.retailer,
|
|
36
|
+
code: entry.code,
|
|
37
|
+
name: entry.codeName,
|
|
38
|
+
family: entry.family,
|
|
39
|
+
penalty: entry.penalty,
|
|
40
|
+
disputableStatus: entry.disputableStatus,
|
|
41
|
+
meaning: entry.meaning,
|
|
42
|
+
meaningPoints: entry.meaningPoints,
|
|
43
|
+
rootCauseCategories: entry.rootCauseCategories,
|
|
44
|
+
rootCauses: entry.rootCauses.length > 0 ? entry.rootCauses : undefined,
|
|
45
|
+
evidenceRequired: entry.evidenceRequired,
|
|
46
|
+
disputable: entry.disputable,
|
|
47
|
+
disputeWindow: entry.disputeWindow,
|
|
48
|
+
prevention: entry.prevention,
|
|
49
|
+
erpPrevention: entry.erpPrevention,
|
|
50
|
+
answer: entry.answer,
|
|
51
|
+
faq: entry.faq,
|
|
52
|
+
url: deductionCodeUrl(entry.slug),
|
|
53
|
+
source: "Pixels and Clicks — pixelsandclicks.net",
|
|
54
|
+
}, null, 2),
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
// --- Tool 2: search_deductions ---
|
|
59
|
+
server.tool("search_deductions", "Search across all retail deduction codes by keyword, symptom description, family (Shortage, Compliance, Pricing, Freight, ASN/EDI, Quality, Returns), or retailer. Returns matching codes ranked by relevance with links to pixelsandclicks.net.", {
|
|
60
|
+
query: z.string().describe("Search term: a keyword, symptom, or description like 'shortage', 'ASN mismatch', 'fill rate'"),
|
|
61
|
+
family: z.enum(["Shortage", "Compliance", "Pricing", "Freight", "ASN/EDI", "Quality", "Returns"]).optional(),
|
|
62
|
+
retailer: z.enum(["Walmart", "Target", "Amazon", "Kroger"]).optional(),
|
|
63
|
+
}, async ({ query, family, retailer }) => {
|
|
64
|
+
const results = searchDeductionCodes(codes, query, { family, retailer });
|
|
65
|
+
if (results.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `No deduction codes matched "${query}". Browse all codes at ${SITE_URL}/deduction-codes`,
|
|
70
|
+
}],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: JSON.stringify({
|
|
77
|
+
query,
|
|
78
|
+
matches: results.slice(0, 10).map((r) => ({
|
|
79
|
+
title: r.title,
|
|
80
|
+
description: r.description,
|
|
81
|
+
url: r.url,
|
|
82
|
+
})),
|
|
83
|
+
total: results.length,
|
|
84
|
+
source: "Pixels and Clicks — pixelsandclicks.net",
|
|
85
|
+
}, null, 2),
|
|
86
|
+
}],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
// --- Tool 3: get_evidence_checklist ---
|
|
90
|
+
server.tool("get_evidence_checklist", "Get the dispute evidence checklist for a specific retail deduction code. Returns the documents and records a supplier needs before filing a dispute, with a link to the full reference on pixelsandclicks.net.", {
|
|
91
|
+
retailer: z.enum(["Walmart", "Target", "Amazon", "Kroger"]),
|
|
92
|
+
code: z.string().describe("Code number or identifier"),
|
|
93
|
+
}, async ({ retailer, code }) => {
|
|
94
|
+
const entry = codes.find((c) => c.retailer === retailer && c.code.toLowerCase() === code.toLowerCase());
|
|
95
|
+
if (!entry) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: `No entry found for ${retailer} code "${code}". Browse all codes: ${SITE_URL}/deduction-codes`,
|
|
100
|
+
}],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
retailer: entry.retailer,
|
|
108
|
+
code: entry.code,
|
|
109
|
+
name: entry.codeName,
|
|
110
|
+
disputableStatus: entry.disputableStatus,
|
|
111
|
+
disputeWindow: entry.disputeWindow,
|
|
112
|
+
evidenceRequired: entry.evidenceRequired,
|
|
113
|
+
disputable: entry.disputable,
|
|
114
|
+
url: deductionCodeUrl(entry.slug),
|
|
115
|
+
source: "Pixels and Clicks — pixelsandclicks.net",
|
|
116
|
+
}, null, 2),
|
|
117
|
+
}],
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
// --- Tool 4: get_prevention_plan ---
|
|
121
|
+
server.tool("get_prevention_plan", "Get the prevention steps and ERP-specific blueprint for a retail deduction code. Includes concrete system configuration for NetSuite or Business Central where available. Links to pixelsandclicks.net.", {
|
|
122
|
+
retailer: z.enum(["Walmart", "Target", "Amazon", "Kroger"]),
|
|
123
|
+
code: z.string().describe("Code number or identifier"),
|
|
124
|
+
erp_system: z.string().optional().describe("ERP system name to filter for, e.g. 'NetSuite', 'Business Central'"),
|
|
125
|
+
}, async ({ retailer, code, erp_system }) => {
|
|
126
|
+
const entry = codes.find((c) => c.retailer === retailer && c.code.toLowerCase() === code.toLowerCase());
|
|
127
|
+
if (!entry) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{
|
|
130
|
+
type: "text",
|
|
131
|
+
text: `No entry found for ${retailer} code "${code}". Browse all codes: ${SITE_URL}/deduction-codes`,
|
|
132
|
+
}],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const result = {
|
|
136
|
+
retailer: entry.retailer,
|
|
137
|
+
code: entry.code,
|
|
138
|
+
name: entry.codeName,
|
|
139
|
+
prevention: entry.prevention,
|
|
140
|
+
};
|
|
141
|
+
if (entry.erpPrevention) {
|
|
142
|
+
if (!erp_system || entry.erpPrevention.system.toLowerCase().includes(erp_system.toLowerCase())) {
|
|
143
|
+
result.erpPrevention = entry.erpPrevention;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
result.erpNote = `ERP blueprint available for ${entry.erpPrevention.system}, but not specifically for "${erp_system}". The general prevention steps above still apply.`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (entry.rootCauseCategories) {
|
|
150
|
+
result.rootCauseCategories = entry.rootCauseCategories;
|
|
151
|
+
}
|
|
152
|
+
result.url = deductionCodeUrl(entry.slug);
|
|
153
|
+
result.source = "Pixels and Clicks — pixelsandclicks.net";
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
// --- Tool 5: diagnose_remittance_line ---
|
|
159
|
+
server.tool("diagnose_remittance_line", "Given a remittance description, code, or symptom, identify which deduction codes match and return a triage summary: what each means, whether it's disputable, and what to do first. Links back to pixelsandclicks.net.", {
|
|
160
|
+
description: z.string().describe("The remittance line text, code number, or a description of the deduction, e.g. 'merchandise billed not shipped', 'shortage on PO 12345', 'ASN accuracy chargeback'"),
|
|
161
|
+
retailer: z.enum(["Walmart", "Target", "Amazon", "Kroger"]).optional(),
|
|
162
|
+
}, async ({ description, retailer }) => {
|
|
163
|
+
// First try exact code match
|
|
164
|
+
const codeMatch = description.match(/\b(?:code\s*)?(\d{1,3})\b/i);
|
|
165
|
+
if (codeMatch && retailer) {
|
|
166
|
+
const exact = codes.find((c) => c.retailer === retailer && c.code === codeMatch[1]);
|
|
167
|
+
if (exact) {
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: JSON.stringify({
|
|
172
|
+
diagnosis: "exact_match",
|
|
173
|
+
match: {
|
|
174
|
+
retailer: exact.retailer,
|
|
175
|
+
code: exact.code,
|
|
176
|
+
name: exact.codeName,
|
|
177
|
+
family: exact.family,
|
|
178
|
+
meaning: exact.meaning,
|
|
179
|
+
disputableStatus: exact.disputableStatus,
|
|
180
|
+
immediateAction: exact.evidenceRequired.length > 0
|
|
181
|
+
? `Gather evidence first: ${exact.evidenceRequired[0]}`
|
|
182
|
+
: exact.disputable ?? "Review dispute options",
|
|
183
|
+
url: deductionCodeUrl(exact.slug),
|
|
184
|
+
},
|
|
185
|
+
source: "Pixels and Clicks — pixelsandclicks.net",
|
|
186
|
+
}, null, 2),
|
|
187
|
+
}],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Fall back to search
|
|
192
|
+
const codeResults = searchDeductionCodes(codes, description, { retailer });
|
|
193
|
+
const articleResults = searchArticles(articles, description);
|
|
194
|
+
if (codeResults.length === 0 && articleResults.length === 0) {
|
|
195
|
+
return {
|
|
196
|
+
content: [{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: `No matching deduction codes or articles found for "${description}". Browse all codes at ${SITE_URL}/deduction-codes or all articles at ${SITE_URL}/articles`,
|
|
199
|
+
}],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const topCodes = codeResults.slice(0, 5);
|
|
203
|
+
const topArticles = articleResults.slice(0, 3);
|
|
204
|
+
const matches = topCodes.map((r) => {
|
|
205
|
+
const code = codes.find((c) => c.slug === r.slug);
|
|
206
|
+
return {
|
|
207
|
+
code: `${code.retailer} ${code.code}`,
|
|
208
|
+
name: code.codeName,
|
|
209
|
+
family: code.family,
|
|
210
|
+
disputableStatus: code.disputableStatus,
|
|
211
|
+
summary: code.answer ?? code.meaning,
|
|
212
|
+
url: r.url,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
content: [{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: JSON.stringify({
|
|
219
|
+
diagnosis: "search_match",
|
|
220
|
+
query: description,
|
|
221
|
+
possibleCodes: matches,
|
|
222
|
+
relatedArticles: topArticles.map((a) => ({
|
|
223
|
+
title: a.title,
|
|
224
|
+
url: a.url,
|
|
225
|
+
})),
|
|
226
|
+
source: "Pixels and Clicks — pixelsandclicks.net",
|
|
227
|
+
}, null, 2),
|
|
228
|
+
}],
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
// --- Tool 6: get_article ---
|
|
232
|
+
server.tool("get_article", "Search for and retrieve a Pixels and Clicks article by topic keyword. Returns the answer field, FAQ, and a link to the full article on pixelsandclicks.net.", {
|
|
233
|
+
topic: z.string().describe("Topic to search for, e.g. 'OTIF fine', 'EDI 856', 'ASN rejections', 'NetSuite EDI'"),
|
|
234
|
+
cluster: z.enum(["prevention", "erp-technical", "forecasting", "recovery", "scoping"]).optional(),
|
|
235
|
+
}, async ({ topic, cluster }) => {
|
|
236
|
+
const results = searchArticles(articles, topic, { cluster });
|
|
237
|
+
if (results.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
content: [{
|
|
240
|
+
type: "text",
|
|
241
|
+
text: `No articles matched "${topic}". Browse all articles at ${SITE_URL}/articles`,
|
|
242
|
+
}],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const topResult = articles.find((a) => a.slug === results[0].slug);
|
|
246
|
+
return {
|
|
247
|
+
content: [{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: JSON.stringify({
|
|
250
|
+
title: topResult.title,
|
|
251
|
+
description: topResult.description,
|
|
252
|
+
answer: topResult.answer,
|
|
253
|
+
answerQualifier: topResult.answerQualifier,
|
|
254
|
+
tags: topResult.tags,
|
|
255
|
+
cluster: topResult.cluster,
|
|
256
|
+
faq: topResult.faq,
|
|
257
|
+
url: articleUrl(topResult.slug),
|
|
258
|
+
otherMatches: results.slice(1, 5).map((r) => ({
|
|
259
|
+
title: r.title,
|
|
260
|
+
url: r.url,
|
|
261
|
+
})),
|
|
262
|
+
source: "Pixels and Clicks — pixelsandclicks.net",
|
|
263
|
+
}, null, 2),
|
|
264
|
+
}],
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
return server;
|
|
268
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface DeductionCode {
|
|
2
|
+
slug: string;
|
|
3
|
+
retailer: "Walmart" | "Target" | "Kroger" | "Amazon";
|
|
4
|
+
code: string;
|
|
5
|
+
codeName: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
draft: boolean;
|
|
9
|
+
family?: string;
|
|
10
|
+
penalty?: string;
|
|
11
|
+
disputableStatus?: string;
|
|
12
|
+
answer?: string;
|
|
13
|
+
answerQualifier?: string;
|
|
14
|
+
meaning: string;
|
|
15
|
+
meaningPoints?: string[];
|
|
16
|
+
rootCauses: string[];
|
|
17
|
+
rootCauseCategories?: {
|
|
18
|
+
physical?: string;
|
|
19
|
+
carrier?: string;
|
|
20
|
+
erp?: string;
|
|
21
|
+
};
|
|
22
|
+
evidenceRequired: string[];
|
|
23
|
+
disputable?: string;
|
|
24
|
+
disputeWindow?: string;
|
|
25
|
+
prevention: string[];
|
|
26
|
+
erpPrevention?: {
|
|
27
|
+
system: string;
|
|
28
|
+
blueprint: string;
|
|
29
|
+
};
|
|
30
|
+
faq: Array<{
|
|
31
|
+
q: string;
|
|
32
|
+
a: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface Article {
|
|
36
|
+
slug: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description: string;
|
|
39
|
+
author: string;
|
|
40
|
+
tags: string[];
|
|
41
|
+
cluster?: string;
|
|
42
|
+
draft: boolean;
|
|
43
|
+
answer?: string;
|
|
44
|
+
answerQualifier?: string;
|
|
45
|
+
faq: Array<{
|
|
46
|
+
q: string;
|
|
47
|
+
a: string;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
export interface SearchResult {
|
|
51
|
+
type: "deduction_code" | "article";
|
|
52
|
+
slug: string;
|
|
53
|
+
title: string;
|
|
54
|
+
description: string;
|
|
55
|
+
relevance: number;
|
|
56
|
+
url: string;
|
|
57
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelsandclicks_/mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server for retail deduction codes, dispute evidence, and supplier-ops knowledge from Pixels and Clicks",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-server": "dist/cli.js",
|
|
8
|
+
"pixelsandclicks-mcp": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/server.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && node scripts/bundle-content.js",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"start": "node dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
18
|
+
"zod": "^3.23.8"
|
|
19
|
+
},
|
|
20
|
+
"optionalDependencies": {
|
|
21
|
+
"gray-matter": "^4.0.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.5.0",
|
|
25
|
+
"@types/node": "^20.14.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"retail",
|
|
30
|
+
"deductions",
|
|
31
|
+
"walmart",
|
|
32
|
+
"amazon",
|
|
33
|
+
"target",
|
|
34
|
+
"supplier",
|
|
35
|
+
"supply-chain"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/pixelsandclicks/pixels-and-clicks",
|
|
45
|
+
"directory": "packages/mcp-server"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://pixelsandclicks.net"
|
|
48
|
+
}
|