@cyanheads/stackexchange-mcp-server 0.1.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.
Files changed (53) hide show
  1. package/AGENTS.md +360 -0
  2. package/CLAUDE.md +360 -0
  3. package/Dockerfile +99 -0
  4. package/LICENSE +201 -0
  5. package/README.md +307 -0
  6. package/changelog/0.1.x/0.1.1.md +27 -0
  7. package/changelog/template.md +127 -0
  8. package/dist/config/server-config.d.ts +11 -0
  9. package/dist/config/server-config.d.ts.map +1 -0
  10. package/dist/config/server-config.js +21 -0
  11. package/dist/config/server-config.js.map +1 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +24 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/mcp-server/tools/definitions/index.d.ts +176 -0
  17. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
  18. package/dist/mcp-server/tools/definitions/index.js +22 -0
  19. package/dist/mcp-server/tools/definitions/index.js.map +1 -0
  20. package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.d.ts +37 -0
  21. package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.d.ts.map +1 -0
  22. package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.js +118 -0
  23. package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.js.map +1 -0
  24. package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.d.ts +54 -0
  25. package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.d.ts.map +1 -0
  26. package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.js +205 -0
  27. package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.js.map +1 -0
  28. package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.d.ts +48 -0
  29. package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.d.ts.map +1 -0
  30. package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.js +151 -0
  31. package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.js.map +1 -0
  32. package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.d.ts +20 -0
  33. package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.d.ts.map +1 -0
  34. package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.js +94 -0
  35. package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.js.map +1 -0
  36. package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.d.ts +45 -0
  37. package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.d.ts.map +1 -0
  38. package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.js +145 -0
  39. package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.js.map +1 -0
  40. package/dist/services/stackexchange/html-normalizer.d.ts +18 -0
  41. package/dist/services/stackexchange/html-normalizer.d.ts.map +1 -0
  42. package/dist/services/stackexchange/html-normalizer.js +143 -0
  43. package/dist/services/stackexchange/html-normalizer.js.map +1 -0
  44. package/dist/services/stackexchange/stackexchange-service.d.ts +143 -0
  45. package/dist/services/stackexchange/stackexchange-service.d.ts.map +1 -0
  46. package/dist/services/stackexchange/stackexchange-service.js +336 -0
  47. package/dist/services/stackexchange/stackexchange-service.js.map +1 -0
  48. package/dist/services/stackexchange/types.d.ts +104 -0
  49. package/dist/services/stackexchange/types.d.ts.map +1 -0
  50. package/dist/services/stackexchange/types.js +7 -0
  51. package/dist/services/stackexchange/types.js.map +1 -0
  52. package/package.json +101 -0
  53. package/server.json +111 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @fileoverview Barrel export for all Stack Exchange MCP tool definitions.
3
+ * @module mcp-server/tools/definitions/index
4
+ */
5
+ export { stackexchangeGetTagFaq } from './stackexchange-get-tag-faq.tool.js';
6
+ export { stackexchangeGetThread } from './stackexchange-get-thread.tool.js';
7
+ export { stackexchangeGetUser } from './stackexchange-get-user.tool.js';
8
+ export { stackexchangeListSites } from './stackexchange-list-sites.tool.js';
9
+ export { stackexchangeSearchQuestions } from './stackexchange-search-questions.tool.js';
10
+ export declare const allToolDefinitions: (import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
11
+ tag: import("zod").ZodString;
12
+ site: import("zod").ZodDefault<import("zod").ZodString>;
13
+ pageSize: import("zod").ZodDefault<import("zod").ZodNumber>;
14
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
15
+ questions: import("zod").ZodArray<import("zod").ZodObject<{
16
+ questionId: import("zod").ZodNumber;
17
+ title: import("zod").ZodString;
18
+ link: import("zod").ZodString;
19
+ score: import("zod").ZodNumber;
20
+ answerCount: import("zod").ZodNumber;
21
+ isAnswered: import("zod").ZodBoolean;
22
+ tags: import("zod").ZodArray<import("zod").ZodString>;
23
+ }, import("zod/v4/core").$strip>>;
24
+ tag: import("zod").ZodString;
25
+ site: import("zod").ZodString;
26
+ }, import("zod/v4/core").$strip>, readonly [{
27
+ readonly reason: "invalid_site";
28
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.InvalidParams;
29
+ readonly when: "The provided site value is not a valid Stack Exchange network site identifier.";
30
+ readonly recovery: "Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.";
31
+ }, {
32
+ readonly reason: "quota_exceeded";
33
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.RateLimited;
34
+ readonly when: "The Stack Exchange API quota_remaining has reached 0.";
35
+ readonly recovery: "Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.";
36
+ }], {
37
+ readonly quotaRemaining: import("zod").ZodNumber;
38
+ readonly quotaMax: import("zod").ZodNumber;
39
+ }> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
40
+ questionIdOrUrl: import("zod").ZodString;
41
+ site: import("zod").ZodDefault<import("zod").ZodString>;
42
+ maxAnswers: import("zod").ZodDefault<import("zod").ZodNumber>;
43
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
44
+ questionId: import("zod").ZodNumber;
45
+ title: import("zod").ZodString;
46
+ link: import("zod").ZodString;
47
+ score: import("zod").ZodNumber;
48
+ tags: import("zod").ZodArray<import("zod").ZodString>;
49
+ bodyMarkdown: import("zod").ZodString;
50
+ authorName: import("zod").ZodOptional<import("zod").ZodString>;
51
+ authorLink: import("zod").ZodOptional<import("zod").ZodString>;
52
+ acceptedAnswerId: import("zod").ZodOptional<import("zod").ZodNumber>;
53
+ answers: import("zod").ZodArray<import("zod").ZodObject<{
54
+ answerId: import("zod").ZodNumber;
55
+ score: import("zod").ZodNumber;
56
+ isAccepted: import("zod").ZodBoolean;
57
+ bodyMarkdown: import("zod").ZodString;
58
+ authorName: import("zod").ZodOptional<import("zod").ZodString>;
59
+ authorLink: import("zod").ZodOptional<import("zod").ZodString>;
60
+ authorReputation: import("zod").ZodOptional<import("zod").ZodNumber>;
61
+ }, import("zod/v4/core").$strip>>;
62
+ }, import("zod/v4/core").$strip>, readonly [{
63
+ readonly reason: "question_not_found";
64
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
65
+ readonly when: "The question lookup returns an empty result set — SE returns HTTP 200 with no items for unknown question IDs rather than 404.";
66
+ readonly recovery: "Verify the question ID or run stackexchange_search_questions to find a valid question ID.";
67
+ }, {
68
+ readonly reason: "invalid_site";
69
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.InvalidParams;
70
+ readonly when: "The provided site value is not a valid Stack Exchange network site identifier.";
71
+ readonly recovery: "Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.";
72
+ }, {
73
+ readonly reason: "invalid_id_or_url";
74
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.InvalidParams;
75
+ readonly when: "The input is not a parseable integer ID and not a recognizable SE question URL.";
76
+ readonly recovery: "Provide a numeric question ID (e.g. \"11227809\") or a valid Stack Exchange question URL.";
77
+ }, {
78
+ readonly reason: "quota_exceeded";
79
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.RateLimited;
80
+ readonly when: "The Stack Exchange API quota_remaining has reached 0.";
81
+ readonly recovery: "Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.";
82
+ }], {
83
+ readonly quotaRemaining: import("zod").ZodNumber;
84
+ readonly quotaMax: import("zod").ZodNumber;
85
+ }> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
86
+ userId: import("zod").ZodNumber;
87
+ site: import("zod").ZodDefault<import("zod").ZodString>;
88
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
89
+ userId: import("zod").ZodNumber;
90
+ displayName: import("zod").ZodString;
91
+ link: import("zod").ZodString;
92
+ reputation: import("zod").ZodNumber;
93
+ badgeCounts: import("zod").ZodOptional<import("zod").ZodObject<{
94
+ gold: import("zod").ZodOptional<import("zod").ZodNumber>;
95
+ silver: import("zod").ZodOptional<import("zod").ZodNumber>;
96
+ bronze: import("zod").ZodOptional<import("zod").ZodNumber>;
97
+ }, import("zod/v4/core").$strip>>;
98
+ location: import("zod").ZodOptional<import("zod").ZodString>;
99
+ websiteUrl: import("zod").ZodOptional<import("zod").ZodString>;
100
+ answerCount: import("zod").ZodOptional<import("zod").ZodNumber>;
101
+ questionCount: import("zod").ZodOptional<import("zod").ZodNumber>;
102
+ topTags: import("zod").ZodArray<import("zod").ZodObject<{
103
+ tagName: import("zod").ZodString;
104
+ answerCount: import("zod").ZodOptional<import("zod").ZodNumber>;
105
+ answerScore: import("zod").ZodOptional<import("zod").ZodNumber>;
106
+ }, import("zod/v4/core").$strip>>;
107
+ }, import("zod/v4/core").$strip>, readonly [{
108
+ readonly reason: "user_not_found";
109
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
110
+ readonly when: "The user lookup returns an empty result set — SE returns HTTP 200 with no items for unknown user IDs rather than 404.";
111
+ readonly recovery: "Verify the user ID or look up a valid ID from an answer via stackexchange_get_thread.";
112
+ }, {
113
+ readonly reason: "invalid_site";
114
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.InvalidParams;
115
+ readonly when: "The provided site value is not a valid Stack Exchange network site identifier.";
116
+ readonly recovery: "Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.";
117
+ }, {
118
+ readonly reason: "quota_exceeded";
119
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.RateLimited;
120
+ readonly when: "The Stack Exchange API quota_remaining has reached 0.";
121
+ readonly recovery: "Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.";
122
+ }], {
123
+ readonly quotaRemaining: import("zod").ZodNumber;
124
+ readonly quotaMax: import("zod").ZodNumber;
125
+ }> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
126
+ filter: import("zod").ZodOptional<import("zod").ZodString>;
127
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
128
+ sites: import("zod").ZodArray<import("zod").ZodObject<{
129
+ name: import("zod").ZodString;
130
+ apiSiteParameter: import("zod").ZodString;
131
+ siteUrl: import("zod").ZodString;
132
+ audience: import("zod").ZodOptional<import("zod").ZodString>;
133
+ }, import("zod/v4/core").$strip>>;
134
+ totalCount: import("zod").ZodNumber;
135
+ }, import("zod/v4/core").$strip>, undefined, {
136
+ readonly quotaRemaining: import("zod").ZodNumber;
137
+ readonly quotaMax: import("zod").ZodNumber;
138
+ }> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
139
+ query: import("zod").ZodString;
140
+ site: import("zod").ZodDefault<import("zod").ZodString>;
141
+ tags: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
142
+ acceptedOnly: import("zod").ZodOptional<import("zod").ZodBoolean>;
143
+ minScore: import("zod").ZodOptional<import("zod").ZodNumber>;
144
+ sort: import("zod").ZodDefault<import("zod").ZodEnum<{
145
+ relevance: "relevance";
146
+ votes: "votes";
147
+ activity: "activity";
148
+ newest: "newest";
149
+ }>>;
150
+ pageSize: import("zod").ZodDefault<import("zod").ZodNumber>;
151
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
152
+ questions: import("zod").ZodArray<import("zod").ZodObject<{
153
+ questionId: import("zod").ZodNumber;
154
+ title: import("zod").ZodString;
155
+ link: import("zod").ZodString;
156
+ score: import("zod").ZodNumber;
157
+ answerCount: import("zod").ZodNumber;
158
+ isAnswered: import("zod").ZodBoolean;
159
+ tags: import("zod").ZodArray<import("zod").ZodString>;
160
+ excerpt: import("zod").ZodOptional<import("zod").ZodString>;
161
+ }, import("zod/v4/core").$strip>>;
162
+ }, import("zod/v4/core").$strip>, readonly [{
163
+ readonly reason: "invalid_site";
164
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.InvalidParams;
165
+ readonly when: "The provided site value is not a valid Stack Exchange network site identifier.";
166
+ readonly recovery: "Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.";
167
+ }, {
168
+ readonly reason: "quota_exceeded";
169
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.RateLimited;
170
+ readonly when: "The Stack Exchange API quota_remaining has reached 0.";
171
+ readonly recovery: "Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.";
172
+ }], {
173
+ readonly quotaRemaining: import("zod").ZodNumber;
174
+ readonly quotaMax: import("zod").ZodNumber;
175
+ }>)[];
176
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AAQxF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAM9B,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview Barrel export for all Stack Exchange MCP tool definitions.
3
+ * @module mcp-server/tools/definitions/index
4
+ */
5
+ export { stackexchangeGetTagFaq } from './stackexchange-get-tag-faq.tool.js';
6
+ export { stackexchangeGetThread } from './stackexchange-get-thread.tool.js';
7
+ export { stackexchangeGetUser } from './stackexchange-get-user.tool.js';
8
+ export { stackexchangeListSites } from './stackexchange-list-sites.tool.js';
9
+ export { stackexchangeSearchQuestions } from './stackexchange-search-questions.tool.js';
10
+ import { stackexchangeGetTagFaq } from './stackexchange-get-tag-faq.tool.js';
11
+ import { stackexchangeGetThread } from './stackexchange-get-thread.tool.js';
12
+ import { stackexchangeGetUser } from './stackexchange-get-user.tool.js';
13
+ import { stackexchangeListSites } from './stackexchange-list-sites.tool.js';
14
+ import { stackexchangeSearchQuestions } from './stackexchange-search-questions.tool.js';
15
+ export const allToolDefinitions = [
16
+ stackexchangeListSites,
17
+ stackexchangeSearchQuestions,
18
+ stackexchangeGetTagFaq,
19
+ stackexchangeGetUser,
20
+ stackexchangeGetThread,
21
+ ];
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AAExF,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AAExF,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,sBAAsB;IACtB,4BAA4B;IAC5B,sBAAsB;IACtB,oBAAoB;IACpB,sBAAsB;CACvB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @fileoverview Tool to fetch the highest-voted answered questions for a tag (tag FAQ).
3
+ * @module mcp-server/tools/definitions/stackexchange-get-tag-faq
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
+ export declare const stackexchangeGetTagFaq: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
8
+ tag: z.ZodString;
9
+ site: z.ZodDefault<z.ZodString>;
10
+ pageSize: z.ZodDefault<z.ZodNumber>;
11
+ }, z.core.$strip>, z.ZodObject<{
12
+ questions: z.ZodArray<z.ZodObject<{
13
+ questionId: z.ZodNumber;
14
+ title: z.ZodString;
15
+ link: z.ZodString;
16
+ score: z.ZodNumber;
17
+ answerCount: z.ZodNumber;
18
+ isAnswered: z.ZodBoolean;
19
+ tags: z.ZodArray<z.ZodString>;
20
+ }, z.core.$strip>>;
21
+ tag: z.ZodString;
22
+ site: z.ZodString;
23
+ }, z.core.$strip>, readonly [{
24
+ readonly reason: "invalid_site";
25
+ readonly code: JsonRpcErrorCode.InvalidParams;
26
+ readonly when: "The provided site value is not a valid Stack Exchange network site identifier.";
27
+ readonly recovery: "Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.";
28
+ }, {
29
+ readonly reason: "quota_exceeded";
30
+ readonly code: JsonRpcErrorCode.RateLimited;
31
+ readonly when: "The Stack Exchange API quota_remaining has reached 0.";
32
+ readonly recovery: "Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.";
33
+ }], {
34
+ readonly quotaRemaining: z.ZodNumber;
35
+ readonly quotaMax: z.ZodNumber;
36
+ }>;
37
+ //# sourceMappingURL=stackexchange-get-tag-faq.tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stackexchange-get-tag-faq.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAGjE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoIjC,CAAC"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @fileoverview Tool to fetch the highest-voted answered questions for a tag (tag FAQ).
3
+ * @module mcp-server/tools/definitions/stackexchange-get-tag-faq
4
+ */
5
+ import { tool, z } from '@cyanheads/mcp-ts-core';
6
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
+ import { getStackExchangeService } from '../../../services/stackexchange/stackexchange-service.js';
8
+ export const stackexchangeGetTagFaq = tool('stackexchange_get_tag_faq', {
9
+ title: 'Get Stack Exchange Tag FAQ',
10
+ description: 'Fetch the highest-voted answered questions for a tag on a Stack Exchange site — the canonical "best answers in X" list. ' +
11
+ 'Returns a question list without bodies; use stackexchange_get_thread to read the full body and answers for any result. ' +
12
+ 'Use this tool to find the authoritative community resources on a topic (e.g. tag "javascript" on stackoverflow). ' +
13
+ 'Use stackexchange_search_questions for free-text search rather than tag-based browsing.',
14
+ annotations: {
15
+ readOnlyHint: true,
16
+ idempotentHint: true,
17
+ openWorldHint: true,
18
+ },
19
+ input: z.object({
20
+ tag: z
21
+ .string()
22
+ .describe('Tag to look up (e.g. "python", "javascript", "docker"). Must match exactly.'),
23
+ site: z
24
+ .string()
25
+ .default('stackoverflow')
26
+ .describe('Stack Exchange site — use the api_site_parameter value (e.g. "stackoverflow", "superuser"). ' +
27
+ 'Defaults to "stackoverflow". Call stackexchange_list_sites to discover valid values.'),
28
+ pageSize: z
29
+ .number()
30
+ .int()
31
+ .min(1)
32
+ .max(30)
33
+ .default(10)
34
+ .describe('Number of results to return (1–30, default 10).'),
35
+ }),
36
+ output: z.object({
37
+ questions: z
38
+ .array(z
39
+ .object({
40
+ questionId: z
41
+ .number()
42
+ .int()
43
+ .describe('Question ID — pass to stackexchange_get_thread to fetch the full thread.'),
44
+ title: z.string().describe('Question title.'),
45
+ link: z.string().describe('Direct URL to the question.'),
46
+ score: z.number().int().describe('Question score (upvotes minus downvotes).'),
47
+ answerCount: z.number().int().describe('Total number of answers.'),
48
+ isAnswered: z
49
+ .boolean()
50
+ .describe('True when the question has an accepted answer or at least one positively-scored answer.'),
51
+ tags: z
52
+ .array(z.string().describe('A tag applied to this question.'))
53
+ .describe('Tags applied to this question.'),
54
+ })
55
+ .describe('A Stack Exchange FAQ question with score, answer count, and tags.'))
56
+ .describe('Highest-voted answered questions for the specified tag, ordered by votes.'),
57
+ tag: z.string().describe('Tag name used for this FAQ lookup.'),
58
+ site: z.string().describe('Stack Exchange site api_site_parameter used for this lookup.'),
59
+ }),
60
+ enrichment: {
61
+ quotaRemaining: z.number().describe('Remaining API quota calls for the current day.'),
62
+ quotaMax: z
63
+ .number()
64
+ .describe('Maximum API quota calls per day (300 keyless, ~10,000 with API key).'),
65
+ },
66
+ enrichmentTrailer: {
67
+ quotaRemaining: { label: 'Quota Remaining' },
68
+ quotaMax: { label: 'Quota Max' },
69
+ },
70
+ errors: [
71
+ {
72
+ reason: 'invalid_site',
73
+ code: JsonRpcErrorCode.InvalidParams,
74
+ when: 'The provided site value is not a valid Stack Exchange network site identifier.',
75
+ recovery: 'Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.',
76
+ },
77
+ {
78
+ reason: 'quota_exceeded',
79
+ code: JsonRpcErrorCode.RateLimited,
80
+ when: 'The Stack Exchange API quota_remaining has reached 0.',
81
+ recovery: 'Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.',
82
+ },
83
+ ],
84
+ async handler(input, ctx) {
85
+ const svc = getStackExchangeService();
86
+ const { questions, quotaRemaining, quotaMax } = await svc.getTagFaq({
87
+ tag: input.tag,
88
+ site: input.site,
89
+ pageSize: input.pageSize,
90
+ }, ctx);
91
+ ctx.enrich({ quotaRemaining, quotaMax });
92
+ if (questions.length === 0) {
93
+ ctx.enrich.notice(`No FAQ questions found for tag "${input.tag}" on ${input.site}. Verify the tag name or try a different site.`);
94
+ }
95
+ ctx.log.info('Fetched SE tag FAQ', {
96
+ tag: input.tag,
97
+ site: input.site,
98
+ count: questions.length,
99
+ });
100
+ return { questions, tag: input.tag, site: input.site };
101
+ },
102
+ format: (result) => {
103
+ const lines = [`## Tag FAQ: \`${result.tag}\` on ${result.site}\n`];
104
+ if (result.questions.length === 0) {
105
+ lines.push('No FAQ questions found for this tag.');
106
+ return [{ type: 'text', text: lines.join('\n') }];
107
+ }
108
+ for (const q of result.questions) {
109
+ lines.push(`### ${q.title}`);
110
+ lines.push(`**ID:** ${q.questionId} | **Score:** ${q.score} | **Answers:** ${q.answerCount} | **Answered:** ${q.isAnswered ? 'Yes' : 'No'}`);
111
+ lines.push(`**Tags:** ${q.tags.join(', ')}`);
112
+ lines.push(`**Link:** ${q.link}`);
113
+ lines.push('');
114
+ }
115
+ return [{ type: 'text', text: lines.join('\n') }];
116
+ },
117
+ });
118
+ //# sourceMappingURL=stackexchange-get-tag-faq.tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stackexchange-get-tag-faq.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fileoverview Tool to fetch a complete Stack Exchange Q&A thread with HTML→markdown normalization.
3
+ * @module mcp-server/tools/definitions/stackexchange-get-thread
4
+ */
5
+ import { z } from '@cyanheads/mcp-ts-core';
6
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
+ export declare const stackexchangeGetThread: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
8
+ questionIdOrUrl: z.ZodString;
9
+ site: z.ZodDefault<z.ZodString>;
10
+ maxAnswers: z.ZodDefault<z.ZodNumber>;
11
+ }, z.core.$strip>, z.ZodObject<{
12
+ questionId: z.ZodNumber;
13
+ title: z.ZodString;
14
+ link: z.ZodString;
15
+ score: z.ZodNumber;
16
+ tags: z.ZodArray<z.ZodString>;
17
+ bodyMarkdown: z.ZodString;
18
+ authorName: z.ZodOptional<z.ZodString>;
19
+ authorLink: z.ZodOptional<z.ZodString>;
20
+ acceptedAnswerId: z.ZodOptional<z.ZodNumber>;
21
+ answers: z.ZodArray<z.ZodObject<{
22
+ answerId: z.ZodNumber;
23
+ score: z.ZodNumber;
24
+ isAccepted: z.ZodBoolean;
25
+ bodyMarkdown: z.ZodString;
26
+ authorName: z.ZodOptional<z.ZodString>;
27
+ authorLink: z.ZodOptional<z.ZodString>;
28
+ authorReputation: z.ZodOptional<z.ZodNumber>;
29
+ }, z.core.$strip>>;
30
+ }, z.core.$strip>, readonly [{
31
+ readonly reason: "question_not_found";
32
+ readonly code: JsonRpcErrorCode.NotFound;
33
+ readonly when: "The question lookup returns an empty result set — SE returns HTTP 200 with no items for unknown question IDs rather than 404.";
34
+ readonly recovery: "Verify the question ID or run stackexchange_search_questions to find a valid question ID.";
35
+ }, {
36
+ readonly reason: "invalid_site";
37
+ readonly code: JsonRpcErrorCode.InvalidParams;
38
+ readonly when: "The provided site value is not a valid Stack Exchange network site identifier.";
39
+ readonly recovery: "Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.";
40
+ }, {
41
+ readonly reason: "invalid_id_or_url";
42
+ readonly code: JsonRpcErrorCode.InvalidParams;
43
+ readonly when: "The input is not a parseable integer ID and not a recognizable SE question URL.";
44
+ readonly recovery: "Provide a numeric question ID (e.g. \"11227809\") or a valid Stack Exchange question URL.";
45
+ }, {
46
+ readonly reason: "quota_exceeded";
47
+ readonly code: JsonRpcErrorCode.RateLimited;
48
+ readonly when: "The Stack Exchange API quota_remaining has reached 0.";
49
+ readonly recovery: "Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.";
50
+ }], {
51
+ readonly quotaRemaining: z.ZodNumber;
52
+ readonly quotaMax: z.ZodNumber;
53
+ }>;
54
+ //# sourceMappingURL=stackexchange-get-thread.tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stackexchange-get-thread.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/stackexchange-get-thread.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA0BjE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmNjC,CAAC"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview Tool to fetch a complete Stack Exchange Q&A thread with HTML→markdown normalization.
3
+ * @module mcp-server/tools/definitions/stackexchange-get-thread
4
+ */
5
+ import { tool, z } from '@cyanheads/mcp-ts-core';
6
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
+ import { getStackExchangeService } from '../../../services/stackexchange/stackexchange-service.js';
8
+ /**
9
+ * Parse a question ID from either a numeric string or a Stack Exchange question URL.
10
+ * Returns null if the input cannot be parsed as a valid SE question ID.
11
+ */
12
+ function parseQuestionIdOrUrl(input) {
13
+ const trimmed = input.trim();
14
+ // Numeric ID directly
15
+ if (/^\d+$/.test(trimmed)) {
16
+ return parseInt(trimmed, 10);
17
+ }
18
+ // Full SE question URL: extract the integer immediately after /questions/
19
+ // Handles: https://stackoverflow.com/questions/11227809/...
20
+ // https://stackoverflow.com/questions/11227809/title#answerAnchor
21
+ const match = trimmed.match(/\/questions\/(\d+)/);
22
+ if (match?.[1]) {
23
+ return parseInt(match[1], 10);
24
+ }
25
+ return null;
26
+ }
27
+ export const stackexchangeGetThread = tool('stackexchange_get_thread', {
28
+ title: 'Get Stack Exchange Q&A Thread',
29
+ description: 'Fetch a complete Q&A thread — question body and all answers, accepted answer first then sorted by score, ' +
30
+ 'rendered as clean markdown with fenced code blocks. Accepts an integer question ID or a full Stack Exchange ' +
31
+ 'question URL (e.g. "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster" or ' +
32
+ '"11227809"). HTML is normalized to markdown automatically; attribution (author + link) included per CC BY-SA 4.0. ' +
33
+ 'Get question IDs from stackexchange_search_questions or stackexchange_get_tag_faq.',
34
+ annotations: {
35
+ readOnlyHint: true,
36
+ idempotentHint: true,
37
+ openWorldHint: true,
38
+ },
39
+ input: z.object({
40
+ questionIdOrUrl: z
41
+ .string()
42
+ .describe('Numeric question ID (e.g. "11227809") or a full Stack Exchange question URL ' +
43
+ '(e.g. "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster"). ' +
44
+ 'The integer immediately following /questions/ is extracted from URLs.'),
45
+ site: z
46
+ .string()
47
+ .default('stackoverflow')
48
+ .describe('Stack Exchange site — use the api_site_parameter value (e.g. "stackoverflow", "superuser"). ' +
49
+ 'Defaults to "stackoverflow". Must match the site where the question lives. ' +
50
+ 'Call stackexchange_list_sites to discover valid values.'),
51
+ maxAnswers: z
52
+ .number()
53
+ .int()
54
+ .min(1)
55
+ .max(100)
56
+ .default(10)
57
+ .describe('Maximum number of answers to include (1–100, default 10). Answers are sorted: accepted first, then by score.'),
58
+ }),
59
+ output: z.object({
60
+ questionId: z
61
+ .number()
62
+ .int()
63
+ .describe('Numeric question ID — identifies this thread on the site.'),
64
+ title: z.string().describe('Question title.'),
65
+ link: z.string().describe('Direct URL to the question.'),
66
+ score: z.number().int().describe('Question score (upvotes minus downvotes).'),
67
+ tags: z
68
+ .array(z.string().describe('A tag applied to this question.'))
69
+ .describe('Tags applied to this question.'),
70
+ bodyMarkdown: z.string().describe('Question body normalized from HTML to markdown.'),
71
+ authorName: z.string().optional().describe('Question author display name when available.'),
72
+ authorLink: z.string().optional().describe('Question author profile URL when available.'),
73
+ acceptedAnswerId: z
74
+ .number()
75
+ .int()
76
+ .optional()
77
+ .describe('ID of the accepted answer when one exists.'),
78
+ answers: z
79
+ .array(z
80
+ .object({
81
+ answerId: z.number().int().describe('Numeric answer ID.'),
82
+ score: z.number().int().describe('Answer score (upvotes minus downvotes).'),
83
+ isAccepted: z.boolean().describe('True when this is the accepted answer.'),
84
+ bodyMarkdown: z.string().describe('Answer body normalized from HTML to markdown.'),
85
+ authorName: z
86
+ .string()
87
+ .optional()
88
+ .describe('Answer author display name when available.'),
89
+ authorLink: z.string().optional().describe('Answer author profile URL when available.'),
90
+ authorReputation: z
91
+ .number()
92
+ .int()
93
+ .optional()
94
+ .describe('Answer author reputation when available.'),
95
+ })
96
+ .describe('A single Q&A answer with markdown body, score, and author attribution.'))
97
+ .describe('Answers sorted: accepted answer first, then by score descending.'),
98
+ }),
99
+ enrichment: {
100
+ quotaRemaining: z.number().describe('Remaining API quota calls for the current day.'),
101
+ quotaMax: z
102
+ .number()
103
+ .describe('Maximum API quota calls per day (300 keyless, ~10,000 with API key).'),
104
+ },
105
+ enrichmentTrailer: {
106
+ quotaRemaining: { label: 'Quota Remaining' },
107
+ quotaMax: { label: 'Quota Max' },
108
+ },
109
+ errors: [
110
+ {
111
+ reason: 'question_not_found',
112
+ code: JsonRpcErrorCode.NotFound,
113
+ when: 'The question lookup returns an empty result set — SE returns HTTP 200 with no items for unknown question IDs rather than 404.',
114
+ recovery: 'Verify the question ID or run stackexchange_search_questions to find a valid question ID.',
115
+ },
116
+ {
117
+ reason: 'invalid_site',
118
+ code: JsonRpcErrorCode.InvalidParams,
119
+ when: 'The provided site value is not a valid Stack Exchange network site identifier.',
120
+ recovery: 'Call stackexchange_list_sites to discover valid site api_site_parameter values and retry.',
121
+ },
122
+ {
123
+ reason: 'invalid_id_or_url',
124
+ code: JsonRpcErrorCode.InvalidParams,
125
+ when: 'The input is not a parseable integer ID and not a recognizable SE question URL.',
126
+ recovery: 'Provide a numeric question ID (e.g. "11227809") or a valid Stack Exchange question URL.',
127
+ },
128
+ {
129
+ reason: 'quota_exceeded',
130
+ code: JsonRpcErrorCode.RateLimited,
131
+ when: 'The Stack Exchange API quota_remaining has reached 0.',
132
+ recovery: 'Quota resets at midnight UTC; set STACKEXCHANGE_API_KEY to lift the limit to 10,000 per day.',
133
+ },
134
+ ],
135
+ async handler(input, ctx) {
136
+ const questionId = parseQuestionIdOrUrl(input.questionIdOrUrl);
137
+ if (questionId === null) {
138
+ throw ctx.fail('invalid_id_or_url', `Cannot parse "${input.questionIdOrUrl}" as a question ID or SE question URL.`, { ...ctx.recoveryFor('invalid_id_or_url'), input: input.questionIdOrUrl });
139
+ }
140
+ const svc = getStackExchangeService();
141
+ const { thread, quotaRemaining, quotaMax } = await svc.getThread({
142
+ questionId,
143
+ site: input.site,
144
+ maxAnswers: input.maxAnswers,
145
+ }, ctx);
146
+ ctx.enrich({ quotaRemaining, quotaMax });
147
+ ctx.log.info('Fetched SE thread', {
148
+ questionId,
149
+ site: input.site,
150
+ answerCount: thread.answers.length,
151
+ });
152
+ return thread;
153
+ },
154
+ format: (result) => {
155
+ const lines = [];
156
+ // Question header
157
+ lines.push(`# ${result.title}`);
158
+ lines.push(`**Question ID:** ${result.questionId} | **Score:** ${result.score}`);
159
+ lines.push(`**Tags:** ${result.tags.join(', ')}`);
160
+ lines.push(`**Link:** ${result.link}`);
161
+ if (result.authorName) {
162
+ const authorRef = result.authorLink
163
+ ? `[${result.authorName}](${result.authorLink})`
164
+ : result.authorName;
165
+ lines.push(`**Author:** ${authorRef}`);
166
+ }
167
+ if (result.acceptedAnswerId != null) {
168
+ lines.push(`**Accepted Answer ID:** ${result.acceptedAnswerId}`);
169
+ }
170
+ lines.push('');
171
+ // Question body
172
+ lines.push('## Question');
173
+ lines.push('');
174
+ lines.push(result.bodyMarkdown);
175
+ lines.push('');
176
+ // Answers
177
+ if (result.answers.length === 0) {
178
+ lines.push('*No answers yet.*');
179
+ }
180
+ else {
181
+ lines.push(`---\n\n## Answers (${result.answers.length})`);
182
+ for (const a of result.answers) {
183
+ lines.push('');
184
+ const acceptedBadge = a.isAccepted ? ' ✓ Accepted' : '';
185
+ lines.push(`### Answer ${a.answerId}${acceptedBadge}`);
186
+ // Attribution per CC BY-SA 4.0
187
+ const attrParts = [`**Score:** ${a.score}`];
188
+ if (a.authorName) {
189
+ const authorRef = a.authorLink ? `[${a.authorName}](${a.authorLink})` : a.authorName;
190
+ attrParts.push(`**Author:** ${authorRef}`);
191
+ if (a.authorReputation != null) {
192
+ attrParts.push(`rep: ${a.authorReputation.toLocaleString()}`);
193
+ }
194
+ }
195
+ lines.push(attrParts.join(' | '));
196
+ lines.push('');
197
+ lines.push(a.bodyMarkdown);
198
+ lines.push('');
199
+ }
200
+ }
201
+ lines.push(`---\n*Content licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)*`);
202
+ return [{ type: 'text', text: lines.join('\n') }];
203
+ },
204
+ });
205
+ //# sourceMappingURL=stackexchange-get-thread.tool.js.map