supporthero-mcp-server 1.0.0 → 1.0.2
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/dist/index.js +1 -0
- package/package.json +13 -5
- package/src/constants.ts +0 -5
- package/src/index.ts +0 -49
- package/src/schemas/inputs.ts +0 -85
- package/src/services/client.ts +0 -96
- package/src/services/formatting.ts +0 -74
- package/src/tools/helpCenter.ts +0 -320
- package/src/types.ts +0 -50
- package/tsconfig.json +0 -19
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supporthero-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "MCP server for SupportHero Help Center API (read-only)",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "MCP server for SupportHero Help Center API (read-only). Enables Claude and other LLMs to search, browse, and read help center articles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"supporthero-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
7
14
|
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
15
|
+
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/tmp && mv dist/tmp dist/index.js",
|
|
9
16
|
"start": "node dist/index.js",
|
|
10
|
-
"dev": "tsc --watch"
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
11
19
|
},
|
|
12
20
|
"dependencies": {
|
|
13
21
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -17,4 +25,4 @@
|
|
|
17
25
|
"@types/node": "^22.0.0",
|
|
18
26
|
"typescript": "^5.7.0"
|
|
19
27
|
}
|
|
20
|
-
}
|
|
28
|
+
}
|
package/src/constants.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { SupportHeroClient } from "./services/client.js";
|
|
4
|
-
import { registerTools } from "./tools/helpCenter.js";
|
|
5
|
-
|
|
6
|
-
// ── Configuration ───────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
const SUPPORTHERO_DOMAIN = process.env.SUPPORTHERO_DOMAIN;
|
|
9
|
-
const SUPPORTHERO_API_KEY = process.env.SUPPORTHERO_API_KEY;
|
|
10
|
-
|
|
11
|
-
if (!SUPPORTHERO_DOMAIN) {
|
|
12
|
-
console.error(
|
|
13
|
-
"Missing SUPPORTHERO_DOMAIN env var. " +
|
|
14
|
-
"Set it to your Help Center URL, e.g. https://yourcompany.supporthero.io"
|
|
15
|
-
);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (!SUPPORTHERO_API_KEY) {
|
|
20
|
-
console.error(
|
|
21
|
-
"Missing SUPPORTHERO_API_KEY env var. " +
|
|
22
|
-
"Find your API key in SupportHero > Settings (bottom of page)."
|
|
23
|
-
);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ── Server Setup ────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
const server = new McpServer({
|
|
30
|
-
name: "supporthero-mcp-server",
|
|
31
|
-
version: "1.0.0",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const client = new SupportHeroClient(SUPPORTHERO_DOMAIN, SUPPORTHERO_API_KEY);
|
|
35
|
-
|
|
36
|
-
registerTools(server, client);
|
|
37
|
-
|
|
38
|
-
// ── Transport ───────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
async function main(): Promise<void> {
|
|
41
|
-
const transport = new StdioServerTransport();
|
|
42
|
-
await server.connect(transport);
|
|
43
|
-
console.error("SupportHero MCP server running on stdio");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
main().catch((error) => {
|
|
47
|
-
console.error("Fatal error:", error);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
});
|
package/src/schemas/inputs.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MAX } from "../constants.js";
|
|
3
|
-
|
|
4
|
-
// ── Categories ──────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
export const ListCategoriesInputSchema = z
|
|
7
|
-
.object({
|
|
8
|
-
parent_category_id: z
|
|
9
|
-
.string()
|
|
10
|
-
.optional()
|
|
11
|
-
.describe(
|
|
12
|
-
"If provided, returns sub-categories of this category. " +
|
|
13
|
-
"If omitted, returns all top-level categories."
|
|
14
|
-
),
|
|
15
|
-
})
|
|
16
|
-
.strict();
|
|
17
|
-
|
|
18
|
-
export type ListCategoriesInput = z.infer<typeof ListCategoriesInputSchema>;
|
|
19
|
-
|
|
20
|
-
export const GetCategoryInputSchema = z
|
|
21
|
-
.object({
|
|
22
|
-
category_id: z
|
|
23
|
-
.string()
|
|
24
|
-
.min(1, "category_id is required")
|
|
25
|
-
.describe("The ID of the category to retrieve."),
|
|
26
|
-
})
|
|
27
|
-
.strict();
|
|
28
|
-
|
|
29
|
-
export type GetCategoryInput = z.infer<typeof GetCategoryInputSchema>;
|
|
30
|
-
|
|
31
|
-
// ── Articles ────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
export const ListArticlesInputSchema = z
|
|
34
|
-
.object({
|
|
35
|
-
category_id: z
|
|
36
|
-
.string()
|
|
37
|
-
.min(1, "category_id is required")
|
|
38
|
-
.describe(
|
|
39
|
-
"The ID of the category whose articles to list. " +
|
|
40
|
-
"Use supporthero_list_categories first to find category IDs."
|
|
41
|
-
),
|
|
42
|
-
})
|
|
43
|
-
.strict();
|
|
44
|
-
|
|
45
|
-
export type ListArticlesInput = z.infer<typeof ListArticlesInputSchema>;
|
|
46
|
-
|
|
47
|
-
export const GetArticleInputSchema = z
|
|
48
|
-
.object({
|
|
49
|
-
article_id: z
|
|
50
|
-
.string()
|
|
51
|
-
.min(1, "article_id is required")
|
|
52
|
-
.describe("The ID of the article to retrieve. Returns full content."),
|
|
53
|
-
})
|
|
54
|
-
.strict();
|
|
55
|
-
|
|
56
|
-
export type GetArticleInput = z.infer<typeof GetArticleInputSchema>;
|
|
57
|
-
|
|
58
|
-
// ── Search ──────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
export const SearchArticlesInputSchema = z
|
|
61
|
-
.object({
|
|
62
|
-
search_text: z
|
|
63
|
-
.string()
|
|
64
|
-
.min(1, "search_text is required")
|
|
65
|
-
.describe("The search query. Matches against article titles and content."),
|
|
66
|
-
max: z
|
|
67
|
-
.number()
|
|
68
|
-
.int()
|
|
69
|
-
.min(1)
|
|
70
|
-
.max(MAX_SEARCH_RESULTS)
|
|
71
|
-
.default(DEFAULT_SEARCH_MAX)
|
|
72
|
-
.describe(
|
|
73
|
-
`Maximum number of results to return per page (1 to ${MAX_SEARCH_RESULTS}, default ${DEFAULT_SEARCH_MAX}).`
|
|
74
|
-
),
|
|
75
|
-
offset: z
|
|
76
|
-
.string()
|
|
77
|
-
.optional()
|
|
78
|
-
.describe(
|
|
79
|
-
"Pagination cursor from a previous search response. " +
|
|
80
|
-
"Pass the offset value from the prior result to get the next page."
|
|
81
|
-
),
|
|
82
|
-
})
|
|
83
|
-
.strict();
|
|
84
|
-
|
|
85
|
-
export type SearchArticlesInput = z.infer<typeof SearchArticlesInputSchema>;
|
package/src/services/client.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { API_VERSION } from "../constants.js";
|
|
2
|
-
import type {
|
|
3
|
-
SHCategory,
|
|
4
|
-
SHCategoryListResponse,
|
|
5
|
-
SHArticle,
|
|
6
|
-
SHArticleListResponse,
|
|
7
|
-
SHSearchResponse,
|
|
8
|
-
} from "../types.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* SupportHero API client.
|
|
12
|
-
* All methods are read-only GET requests against the Help Center API.
|
|
13
|
-
*/
|
|
14
|
-
export class SupportHeroClient {
|
|
15
|
-
private baseUrl: string;
|
|
16
|
-
private apiKey: string;
|
|
17
|
-
|
|
18
|
-
constructor(baseUrl: string, apiKey: string) {
|
|
19
|
-
// Normalize: strip trailing slash
|
|
20
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
21
|
-
this.apiKey = apiKey;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Core fetch wrapper. Appends apiKey and handles errors uniformly.
|
|
26
|
-
*/
|
|
27
|
-
private async request<T>(path: string, params: Record<string, string> = {}): Promise<T> {
|
|
28
|
-
const url = new URL(`/api/${API_VERSION}/${path}`, this.baseUrl);
|
|
29
|
-
url.searchParams.set("apiKey", this.apiKey);
|
|
30
|
-
for (const [key, value] of Object.entries(params)) {
|
|
31
|
-
if (value !== undefined && value !== "") {
|
|
32
|
-
url.searchParams.set(key, value);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const response = await fetch(url.toString(), {
|
|
37
|
-
method: "GET",
|
|
38
|
-
headers: { "Accept": "application/json" },
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
const body = await response.text().catch(() => "");
|
|
43
|
-
throw new Error(
|
|
44
|
-
`SupportHero API error ${response.status}: ${response.statusText}. ${body}`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return response.json() as Promise<T>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ── Categories ──────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
/** List top-level categories, or sub-categories of a given parent. */
|
|
54
|
-
async listCategories(parentCategoryId?: string): Promise<SHCategory[]> {
|
|
55
|
-
const params: Record<string, string> = {};
|
|
56
|
-
if (parentCategoryId) {
|
|
57
|
-
params.categoryId = parentCategoryId;
|
|
58
|
-
}
|
|
59
|
-
const data = await this.request<SHCategoryListResponse>("category", params);
|
|
60
|
-
return data.categories;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Get a single category by ID. */
|
|
64
|
-
async getCategory(categoryId: string): Promise<SHCategory> {
|
|
65
|
-
return this.request<SHCategory>(`category/${categoryId}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Articles ────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
/** List all articles in a given category. */
|
|
71
|
-
async listArticles(categoryId: string): Promise<SHArticle[]> {
|
|
72
|
-
const data = await this.request<SHArticleListResponse>("article", {
|
|
73
|
-
categoryId,
|
|
74
|
-
});
|
|
75
|
-
return data.articles;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Get a single article by ID. */
|
|
79
|
-
async getArticle(articleId: string): Promise<SHArticle> {
|
|
80
|
-
return this.request<SHArticle>(`article/${articleId}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── Search ──────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
/** Full-text search across all articles. Returns paginated results. */
|
|
86
|
-
async searchArticles(
|
|
87
|
-
searchText: string,
|
|
88
|
-
max?: number,
|
|
89
|
-
offset?: string
|
|
90
|
-
): Promise<SHSearchResponse> {
|
|
91
|
-
const params: Record<string, string> = { searchText };
|
|
92
|
-
if (max !== undefined) params.max = String(max);
|
|
93
|
-
if (offset) params.offset = offset;
|
|
94
|
-
return this.request<SHSearchResponse>("search", params);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { SHCategory, SHArticle } from "../types.js";
|
|
2
|
-
import { CHARACTER_LIMIT } from "../constants.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Strip HTML tags from article body to produce plain text.
|
|
6
|
-
* Keeps it simple since we just need readable content for the LLM.
|
|
7
|
-
*/
|
|
8
|
-
export function stripHtml(html: string): string {
|
|
9
|
-
return html
|
|
10
|
-
.replace(/<br\s*\/?>/gi, "\n")
|
|
11
|
-
.replace(/<\/p>/gi, "\n\n")
|
|
12
|
-
.replace(/<\/li>/gi, "\n")
|
|
13
|
-
.replace(/<\/h[1-6]>/gi, "\n\n")
|
|
14
|
-
.replace(/<[^>]+>/g, "")
|
|
15
|
-
.replace(/&/g, "&")
|
|
16
|
-
.replace(/</g, "<")
|
|
17
|
-
.replace(/>/g, ">")
|
|
18
|
-
.replace(/"/g, '"')
|
|
19
|
-
.replace(/'/g, "'")
|
|
20
|
-
.replace(/ /g, " ")
|
|
21
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
22
|
-
.trim();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Format a single category for display. */
|
|
26
|
-
export function formatCategory(cat: SHCategory): string {
|
|
27
|
-
return [
|
|
28
|
-
`Category: ${cat.name} (ID: ${cat.id})`,
|
|
29
|
-
` Level: ${cat.level}`,
|
|
30
|
-
` Published articles: ${cat.articlePublishedCount}`,
|
|
31
|
-
` Total articles: ${cat.articleCount}`,
|
|
32
|
-
` Sub-categories: ${cat.categoriesCount}`,
|
|
33
|
-
cat.parentId !== "0" ? ` Parent ID: ${cat.parentId}` : null,
|
|
34
|
-
` Slug: ${cat.slug}`,
|
|
35
|
-
]
|
|
36
|
-
.filter(Boolean)
|
|
37
|
-
.join("\n");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Format a single article for display. Optionally include body content. */
|
|
41
|
-
export function formatArticle(article: SHArticle, includeBody: boolean = false): string {
|
|
42
|
-
const lines: string[] = [
|
|
43
|
-
`Title: ${article.title} (ID: ${article.id})`,
|
|
44
|
-
` Category ID: ${article.categoryId}`,
|
|
45
|
-
` Type: ${article.type}`,
|
|
46
|
-
` Published: ${article.published ? "yes" : "no"}`,
|
|
47
|
-
` Created: ${article.dateCreated}`,
|
|
48
|
-
` Updated: ${article.lastUpdated}`,
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
if (article.keywords) {
|
|
52
|
-
lines.push(` Keywords: ${article.keywords}`);
|
|
53
|
-
}
|
|
54
|
-
if (article.slug) {
|
|
55
|
-
lines.push(` Slug: ${article.slug}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (includeBody && article.description) {
|
|
59
|
-
const body = stripHtml(article.description);
|
|
60
|
-
lines.push("");
|
|
61
|
-
lines.push("--- Content ---");
|
|
62
|
-
lines.push(body);
|
|
63
|
-
lines.push("--- End Content ---");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return lines.join("\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Truncate text if it exceeds the character limit, with a warning. */
|
|
70
|
-
export function truncateIfNeeded(text: string): string {
|
|
71
|
-
if (text.length <= CHARACTER_LIMIT) return text;
|
|
72
|
-
const truncated = text.slice(0, CHARACTER_LIMIT);
|
|
73
|
-
return truncated + "\n\n[Response truncated. Use more specific queries or pagination to get remaining results.]";
|
|
74
|
-
}
|
package/src/tools/helpCenter.ts
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { SupportHeroClient } from "../services/client.js";
|
|
3
|
-
import {
|
|
4
|
-
formatCategory,
|
|
5
|
-
formatArticle,
|
|
6
|
-
truncateIfNeeded,
|
|
7
|
-
} from "../services/formatting.js";
|
|
8
|
-
import {
|
|
9
|
-
ListCategoriesInputSchema,
|
|
10
|
-
GetCategoryInputSchema,
|
|
11
|
-
ListArticlesInputSchema,
|
|
12
|
-
GetArticleInputSchema,
|
|
13
|
-
SearchArticlesInputSchema,
|
|
14
|
-
} from "../schemas/inputs.js";
|
|
15
|
-
import type {
|
|
16
|
-
ListCategoriesInput,
|
|
17
|
-
GetCategoryInput,
|
|
18
|
-
ListArticlesInput,
|
|
19
|
-
GetArticleInput,
|
|
20
|
-
SearchArticlesInput,
|
|
21
|
-
} from "../schemas/inputs.js";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Register all SupportHero tools on the given MCP server.
|
|
25
|
-
*/
|
|
26
|
-
export function registerTools(server: McpServer, client: SupportHeroClient): void {
|
|
27
|
-
// ── List Categories ───────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
server.registerTool(
|
|
30
|
-
"supporthero_list_categories",
|
|
31
|
-
{
|
|
32
|
-
title: "List Help Center Categories",
|
|
33
|
-
description: `List categories in the Propel Help Center.
|
|
34
|
-
|
|
35
|
-
By default returns all top-level categories. Pass parent_category_id to get
|
|
36
|
-
sub-categories of a specific category.
|
|
37
|
-
|
|
38
|
-
Use this to discover the category tree before listing articles. Categories
|
|
39
|
-
can be nested up to 3 levels deep.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
- parent_category_id (string, optional): ID of parent category to get sub-categories for
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
A list of categories with their IDs, names, article counts, and sub-category counts.
|
|
46
|
-
Use the category ID with supporthero_list_articles to get articles in that category,
|
|
47
|
-
or with this tool again to drill into sub-categories.`,
|
|
48
|
-
inputSchema: ListCategoriesInputSchema,
|
|
49
|
-
annotations: {
|
|
50
|
-
readOnlyHint: true,
|
|
51
|
-
destructiveHint: false,
|
|
52
|
-
idempotentHint: true,
|
|
53
|
-
openWorldHint: true,
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
async (params: ListCategoriesInput) => {
|
|
57
|
-
try {
|
|
58
|
-
const categories = await client.listCategories(params.parent_category_id);
|
|
59
|
-
|
|
60
|
-
if (categories.length === 0) {
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
{
|
|
64
|
-
type: "text" as const,
|
|
65
|
-
text: params.parent_category_id
|
|
66
|
-
? `No sub-categories found under category ${params.parent_category_id}.`
|
|
67
|
-
: "No categories found in the Help Center.",
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const text = categories.map(formatCategory).join("\n\n");
|
|
74
|
-
return {
|
|
75
|
-
content: [{ type: "text" as const, text: truncateIfNeeded(text) }],
|
|
76
|
-
};
|
|
77
|
-
} catch (error) {
|
|
78
|
-
return {
|
|
79
|
-
isError: true,
|
|
80
|
-
content: [
|
|
81
|
-
{
|
|
82
|
-
type: "text" as const,
|
|
83
|
-
text: `Error listing categories: ${error instanceof Error ? error.message : String(error)}`,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// ── Get Category ──────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
server.registerTool(
|
|
94
|
-
"supporthero_get_category",
|
|
95
|
-
{
|
|
96
|
-
title: "Get Help Center Category",
|
|
97
|
-
description: `Get details of a specific Help Center category by ID.
|
|
98
|
-
|
|
99
|
-
Returns the category name, article counts (total, local, published),
|
|
100
|
-
sub-category count, tree level, and slug.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
- category_id (string, required): The category ID
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
Full category details including name, article counts, position, and slug.`,
|
|
107
|
-
inputSchema: GetCategoryInputSchema,
|
|
108
|
-
annotations: {
|
|
109
|
-
readOnlyHint: true,
|
|
110
|
-
destructiveHint: false,
|
|
111
|
-
idempotentHint: true,
|
|
112
|
-
openWorldHint: true,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
async (params: GetCategoryInput) => {
|
|
116
|
-
try {
|
|
117
|
-
const category = await client.getCategory(params.category_id);
|
|
118
|
-
return {
|
|
119
|
-
content: [{ type: "text" as const, text: formatCategory(category) }],
|
|
120
|
-
};
|
|
121
|
-
} catch (error) {
|
|
122
|
-
return {
|
|
123
|
-
isError: true,
|
|
124
|
-
content: [
|
|
125
|
-
{
|
|
126
|
-
type: "text" as const,
|
|
127
|
-
text: `Error getting category ${params.category_id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// ── List Articles ─────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
server.registerTool(
|
|
138
|
-
"supporthero_list_articles",
|
|
139
|
-
{
|
|
140
|
-
title: "List Articles in Category",
|
|
141
|
-
description: `List all articles in a specific Help Center category.
|
|
142
|
-
|
|
143
|
-
Returns article titles, IDs, types, publication status, and dates.
|
|
144
|
-
Does NOT include full article body content (use supporthero_get_article for that).
|
|
145
|
-
|
|
146
|
-
To find category IDs, use supporthero_list_categories first.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
- category_id (string, required): The category ID to list articles from
|
|
150
|
-
|
|
151
|
-
Returns:
|
|
152
|
-
A list of article summaries (title, ID, type, published status, dates, keywords).
|
|
153
|
-
Use the article ID with supporthero_get_article to read the full content.`,
|
|
154
|
-
inputSchema: ListArticlesInputSchema,
|
|
155
|
-
annotations: {
|
|
156
|
-
readOnlyHint: true,
|
|
157
|
-
destructiveHint: false,
|
|
158
|
-
idempotentHint: true,
|
|
159
|
-
openWorldHint: true,
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
async (params: ListArticlesInput) => {
|
|
163
|
-
try {
|
|
164
|
-
const articles = await client.listArticles(params.category_id);
|
|
165
|
-
|
|
166
|
-
if (articles.length === 0) {
|
|
167
|
-
return {
|
|
168
|
-
content: [
|
|
169
|
-
{
|
|
170
|
-
type: "text" as const,
|
|
171
|
-
text: `No articles found in category ${params.category_id}.`,
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const text = articles
|
|
178
|
-
.map((a) => formatArticle(a, false))
|
|
179
|
-
.join("\n\n");
|
|
180
|
-
return {
|
|
181
|
-
content: [{ type: "text" as const, text: truncateIfNeeded(text) }],
|
|
182
|
-
};
|
|
183
|
-
} catch (error) {
|
|
184
|
-
return {
|
|
185
|
-
isError: true,
|
|
186
|
-
content: [
|
|
187
|
-
{
|
|
188
|
-
type: "text" as const,
|
|
189
|
-
text: `Error listing articles for category ${params.category_id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
// ── Get Article ───────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
server.registerTool(
|
|
200
|
-
"supporthero_get_article",
|
|
201
|
-
{
|
|
202
|
-
title: "Get Help Center Article",
|
|
203
|
-
description: `Get the full content of a specific Help Center article by ID.
|
|
204
|
-
|
|
205
|
-
Returns the article title, metadata, and full body content (HTML stripped to plain text).
|
|
206
|
-
Use this to read the actual content of an article found via search or category listing.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
- article_id (string, required): The article ID
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
Full article including title, category ID, type, publication status, dates,
|
|
213
|
-
keywords, slug, and the complete article body as plain text.`,
|
|
214
|
-
inputSchema: GetArticleInputSchema,
|
|
215
|
-
annotations: {
|
|
216
|
-
readOnlyHint: true,
|
|
217
|
-
destructiveHint: false,
|
|
218
|
-
idempotentHint: true,
|
|
219
|
-
openWorldHint: true,
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
async (params: GetArticleInput) => {
|
|
223
|
-
try {
|
|
224
|
-
const article = await client.getArticle(params.article_id);
|
|
225
|
-
const text = formatArticle(article, true);
|
|
226
|
-
return {
|
|
227
|
-
content: [{ type: "text" as const, text: truncateIfNeeded(text) }],
|
|
228
|
-
};
|
|
229
|
-
} catch (error) {
|
|
230
|
-
return {
|
|
231
|
-
isError: true,
|
|
232
|
-
content: [
|
|
233
|
-
{
|
|
234
|
-
type: "text" as const,
|
|
235
|
-
text: `Error getting article ${params.article_id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// ── Search Articles ───────────────────────────────────────
|
|
244
|
-
|
|
245
|
-
server.registerTool(
|
|
246
|
-
"supporthero_search_articles",
|
|
247
|
-
{
|
|
248
|
-
title: "Search Help Center Articles",
|
|
249
|
-
description: `Full-text search across all Propel Help Center articles.
|
|
250
|
-
|
|
251
|
-
Searches article titles and body content. Returns paginated results with
|
|
252
|
-
article summaries (no full body). Use supporthero_get_article to read
|
|
253
|
-
the full content of a specific result.
|
|
254
|
-
|
|
255
|
-
Pagination: The response includes a total count and an offset cursor.
|
|
256
|
-
Pass the offset value from one response into the next request to page
|
|
257
|
-
through results.
|
|
258
|
-
|
|
259
|
-
Args:
|
|
260
|
-
- search_text (string, required): The search query
|
|
261
|
-
- max (number, optional): Results per page, 1 to 100 (default 10)
|
|
262
|
-
- offset (string, optional): Pagination cursor from previous search response
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
A list of matching articles (title, ID, type, published status, dates),
|
|
266
|
-
total number of matches, and a pagination offset (null if no more results).`,
|
|
267
|
-
inputSchema: SearchArticlesInputSchema,
|
|
268
|
-
annotations: {
|
|
269
|
-
readOnlyHint: true,
|
|
270
|
-
destructiveHint: false,
|
|
271
|
-
idempotentHint: true,
|
|
272
|
-
openWorldHint: true,
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
async (params: SearchArticlesInput) => {
|
|
276
|
-
try {
|
|
277
|
-
const result = await client.searchArticles(
|
|
278
|
-
params.search_text,
|
|
279
|
-
params.max,
|
|
280
|
-
params.offset
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
if (result.articles.length === 0) {
|
|
284
|
-
return {
|
|
285
|
-
content: [
|
|
286
|
-
{
|
|
287
|
-
type: "text" as const,
|
|
288
|
-
text: `No articles found matching "${params.search_text}".`,
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const header = `Search results for "${params.search_text}": ${result.total} total matches\n`;
|
|
295
|
-
const articleText = result.articles
|
|
296
|
-
.map((a) => formatArticle(a, false))
|
|
297
|
-
.join("\n\n");
|
|
298
|
-
const footer = result.offset
|
|
299
|
-
? `\nMore results available. Pass offset "${result.offset}" to get the next page.`
|
|
300
|
-
: "\nNo more results.";
|
|
301
|
-
|
|
302
|
-
const text = header + "\n" + articleText + footer;
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
content: [{ type: "text" as const, text: truncateIfNeeded(text) }],
|
|
306
|
-
};
|
|
307
|
-
} catch (error) {
|
|
308
|
-
return {
|
|
309
|
-
isError: true,
|
|
310
|
-
content: [
|
|
311
|
-
{
|
|
312
|
-
type: "text" as const,
|
|
313
|
-
text: `Error searching articles: ${error instanceof Error ? error.message : String(error)}`,
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
);
|
|
320
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// SupportHero API response types
|
|
2
|
-
|
|
3
|
-
export interface SHCategory {
|
|
4
|
-
id: string;
|
|
5
|
-
articleCount: number;
|
|
6
|
-
articleLocalCount: number;
|
|
7
|
-
articlePublishedCount: number;
|
|
8
|
-
categoriesCount: number;
|
|
9
|
-
level: number;
|
|
10
|
-
parentId: string;
|
|
11
|
-
name: string;
|
|
12
|
-
position: number;
|
|
13
|
-
slug: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SHCategoryListResponse {
|
|
17
|
-
categories: SHCategory[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface SHArticle {
|
|
21
|
-
id: string;
|
|
22
|
-
categoryId: string;
|
|
23
|
-
countryCode: string;
|
|
24
|
-
dateCreated: string;
|
|
25
|
-
description: string; // HTML body content
|
|
26
|
-
keywords: string;
|
|
27
|
-
language: string;
|
|
28
|
-
lastUpdated: string;
|
|
29
|
-
metaCanonical: string;
|
|
30
|
-
metaDescription: string;
|
|
31
|
-
metaKeywords: string;
|
|
32
|
-
metaTitle: string;
|
|
33
|
-
position: number;
|
|
34
|
-
published: boolean;
|
|
35
|
-
slug: string;
|
|
36
|
-
title: string;
|
|
37
|
-
type: "article" | "tutorial";
|
|
38
|
-
videoThumbnailUrl: string;
|
|
39
|
-
videoUrl: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface SHArticleListResponse {
|
|
43
|
-
articles: SHArticle[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface SHSearchResponse {
|
|
47
|
-
articles: SHArticle[];
|
|
48
|
-
total: number;
|
|
49
|
-
offset: string | null;
|
|
50
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "Node16",
|
|
5
|
-
"moduleResolution": "Node16",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"declaration": true,
|
|
14
|
-
"declarationMap": true,
|
|
15
|
-
"sourceMap": true
|
|
16
|
-
},
|
|
17
|
-
"include": ["src/**/*"],
|
|
18
|
-
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|