@nicnocquee/dataqueue 1.32.0 → 1.34.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.
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DataQueue MCP Server — exposes documentation search over stdio.
5
+ * Run via: dataqueue-cli mcp
6
+ */
7
+
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { z } from 'zod';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ interface DocPage {
19
+ slug: string;
20
+ title: string;
21
+ description: string;
22
+ content: string;
23
+ }
24
+
25
+ /** @internal Loads docs-content.json from the ai/ directory bundled with the package. */
26
+ export function loadDocsContent(
27
+ docsPath: string = path.join(__dirname, '../ai/docs-content.json'),
28
+ ): DocPage[] {
29
+ const raw = fs.readFileSync(docsPath, 'utf-8');
30
+ return JSON.parse(raw) as DocPage[];
31
+ }
32
+
33
+ /** @internal Scores a doc page against a search query using simple term matching. */
34
+ export function scorePageForQuery(page: DocPage, queryTerms: string[]): number {
35
+ const titleLower = page.title.toLowerCase();
36
+ const descLower = page.description.toLowerCase();
37
+ const contentLower = page.content.toLowerCase();
38
+
39
+ let score = 0;
40
+ for (const term of queryTerms) {
41
+ if (titleLower.includes(term)) score += 10;
42
+ if (descLower.includes(term)) score += 5;
43
+
44
+ const contentMatches = contentLower.split(term).length - 1;
45
+ score += Math.min(contentMatches, 10);
46
+ }
47
+ return score;
48
+ }
49
+
50
+ /** @internal Extracts a relevant excerpt around the first match of any query term. */
51
+ export function extractExcerpt(
52
+ content: string,
53
+ queryTerms: string[],
54
+ maxLength = 500,
55
+ ): string {
56
+ const lower = content.toLowerCase();
57
+ let earliestIndex = -1;
58
+
59
+ for (const term of queryTerms) {
60
+ const idx = lower.indexOf(term);
61
+ if (idx !== -1 && (earliestIndex === -1 || idx < earliestIndex)) {
62
+ earliestIndex = idx;
63
+ }
64
+ }
65
+
66
+ if (earliestIndex === -1) {
67
+ return content.slice(0, maxLength);
68
+ }
69
+
70
+ const start = Math.max(0, earliestIndex - 100);
71
+ const end = Math.min(content.length, start + maxLength);
72
+ let excerpt = content.slice(start, end);
73
+
74
+ if (start > 0) excerpt = '...' + excerpt;
75
+ if (end < content.length) excerpt = excerpt + '...';
76
+
77
+ return excerpt;
78
+ }
79
+
80
+ /**
81
+ * Creates and starts the DataQueue MCP server over stdio.
82
+ *
83
+ * @param deps - Injectable dependencies for testing.
84
+ */
85
+ export async function startMcpServer(
86
+ deps: {
87
+ docsPath?: string;
88
+ transport?: InstanceType<typeof StdioServerTransport>;
89
+ } = {},
90
+ ): Promise<McpServer> {
91
+ const pages = loadDocsContent(deps.docsPath);
92
+
93
+ const server = new McpServer({
94
+ name: 'dataqueue-docs',
95
+ version: '1.0.0',
96
+ });
97
+
98
+ server.resource('llms-txt', 'dataqueue://llms.txt', async () => {
99
+ const llmsPath = path.join(
100
+ __dirname,
101
+ '../ai/skills/dataqueue-core/SKILL.md',
102
+ );
103
+ let content: string;
104
+ try {
105
+ content = fs.readFileSync(llmsPath, 'utf-8');
106
+ } catch {
107
+ content = pages
108
+ .map((p) => `## ${p.title}\n\nSlug: ${p.slug}\n\n${p.description}`)
109
+ .join('\n\n');
110
+ }
111
+ return { contents: [{ uri: 'dataqueue://llms.txt', text: content }] };
112
+ });
113
+
114
+ server.tool(
115
+ 'list-doc-pages',
116
+ 'List all available DataQueue documentation pages with titles and descriptions.',
117
+ {},
118
+ async () => {
119
+ const listing = pages.map((p) => ({
120
+ slug: p.slug,
121
+ title: p.title,
122
+ description: p.description,
123
+ }));
124
+ return {
125
+ content: [
126
+ { type: 'text' as const, text: JSON.stringify(listing, null, 2) },
127
+ ],
128
+ };
129
+ },
130
+ );
131
+
132
+ server.tool(
133
+ 'get-doc-page',
134
+ 'Fetch a specific DataQueue doc page by slug. Returns full page content as markdown.',
135
+ {
136
+ slug: z
137
+ .string()
138
+ .describe('The doc page slug, e.g. "usage/add-job" or "api/job-queue"'),
139
+ },
140
+ async ({ slug }) => {
141
+ const page = pages.find((p) => p.slug === slug);
142
+ if (!page) {
143
+ return {
144
+ content: [
145
+ {
146
+ type: 'text' as const,
147
+ text: `Page not found: "${slug}". Use list-doc-pages to see available slugs.`,
148
+ },
149
+ ],
150
+ isError: true,
151
+ };
152
+ }
153
+ const header = page.description
154
+ ? `# ${page.title}\n\n> ${page.description}\n\n`
155
+ : `# ${page.title}\n\n`;
156
+ return {
157
+ content: [{ type: 'text' as const, text: header + page.content }],
158
+ };
159
+ },
160
+ );
161
+
162
+ server.tool(
163
+ 'search-docs',
164
+ 'Full-text search across all DataQueue documentation pages. Returns matching sections with page titles and content excerpts.',
165
+ {
166
+ query: z
167
+ .string()
168
+ .describe('Search query, e.g. "cron scheduling" or "waitForToken"'),
169
+ },
170
+ async ({ query }) => {
171
+ const queryTerms = query
172
+ .toLowerCase()
173
+ .split(/\s+/)
174
+ .filter((t) => t.length > 1);
175
+
176
+ if (queryTerms.length === 0) {
177
+ return {
178
+ content: [
179
+ { type: 'text' as const, text: 'Please provide a search query.' },
180
+ ],
181
+ isError: true,
182
+ };
183
+ }
184
+
185
+ const scored = pages
186
+ .map((page) => ({
187
+ page,
188
+ score: scorePageForQuery(page, queryTerms),
189
+ }))
190
+ .filter((r) => r.score > 0)
191
+ .sort((a, b) => b.score - a.score)
192
+ .slice(0, 5);
193
+
194
+ if (scored.length === 0) {
195
+ return {
196
+ content: [
197
+ {
198
+ type: 'text' as const,
199
+ text: `No results for "${query}". Try different keywords or use list-doc-pages to browse.`,
200
+ },
201
+ ],
202
+ };
203
+ }
204
+
205
+ const results = scored.map((r) => {
206
+ const excerpt = extractExcerpt(r.page.content, queryTerms);
207
+ return `## ${r.page.title} (${r.page.slug})\n\n${r.page.description}\n\n${excerpt}`;
208
+ });
209
+
210
+ return {
211
+ content: [{ type: 'text' as const, text: results.join('\n\n---\n\n') }],
212
+ };
213
+ },
214
+ );
215
+
216
+ const transport = deps.transport ?? new StdioServerTransport();
217
+ await server.connect(transport);
218
+ return server;
219
+ }
220
+
221
+ const isDirectRun =
222
+ process.argv[1] &&
223
+ (process.argv[1].endsWith('/mcp-server.js') ||
224
+ process.argv[1].endsWith('/mcp-server.cjs'));
225
+
226
+ if (isDirectRun) {
227
+ startMcpServer().catch((err) => {
228
+ console.error('Failed to start MCP server:', err);
229
+ process.exit(1);
230
+ });
231
+ }