@answer-engine/mcp-server 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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { AnswerEngineClient, ApiError } from '../api-client.js';
3
+ // Mock global fetch
4
+ const mockFetch = vi.fn();
5
+ global.fetch = mockFetch;
6
+ describe('AnswerEngineClient', () => {
7
+ let client;
8
+ beforeEach(() => {
9
+ client = new AnswerEngineClient({ apiUrl: 'http://localhost:5050', apiKey: 'ae_live_test123' });
10
+ mockFetch.mockReset();
11
+ });
12
+ describe('constructor', () => {
13
+ it('strips trailing slashes from apiUrl', async () => {
14
+ const c = new AnswerEngineClient({ apiUrl: 'http://example.com///', apiKey: 'key' });
15
+ mockFetch.mockResolvedValueOnce({
16
+ ok: true,
17
+ json: async () => ({ success: true, data: { contentTypes: {}, tags: [], capabilities: [], dateRange: { earliest: null, latest: null } }, creditsCharged: 0, creditsRemaining: 100 }),
18
+ });
19
+ await c.getSchema();
20
+ expect(mockFetch).toHaveBeenCalledWith('http://example.com/api/v1/agent/schema', expect.any(Object));
21
+ });
22
+ });
23
+ describe('request handling', () => {
24
+ it('sends X-API-Key header', async () => {
25
+ mockFetch.mockResolvedValueOnce({
26
+ ok: true,
27
+ json: async () => ({ success: true, data: { contentTypes: {}, tags: [], capabilities: [], dateRange: { earliest: null, latest: null } }, creditsCharged: 0, creditsRemaining: 100 }),
28
+ });
29
+ await client.getSchema();
30
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:5050/api/v1/agent/schema', expect.objectContaining({
31
+ headers: expect.objectContaining({ 'X-API-Key': 'ae_live_test123' }),
32
+ }));
33
+ });
34
+ it('throws ApiError on non-ok response', async () => {
35
+ mockFetch.mockResolvedValue({
36
+ ok: false,
37
+ status: 401,
38
+ json: async () => ({ error: { code: 'INVALID_API_KEY', message: 'Invalid key' } }),
39
+ });
40
+ await expect(client.getSchema()).rejects.toThrow(ApiError);
41
+ await expect(client.getSchema()).rejects.toMatchObject({
42
+ statusCode: 401,
43
+ code: 'INVALID_API_KEY',
44
+ });
45
+ });
46
+ it('handles missing error body gracefully', async () => {
47
+ mockFetch.mockResolvedValueOnce({
48
+ ok: false,
49
+ status: 500,
50
+ json: async () => ({}),
51
+ });
52
+ await expect(client.getSchema()).rejects.toThrow(ApiError);
53
+ });
54
+ it('sends POST body as JSON for query', async () => {
55
+ mockFetch.mockResolvedValueOnce({
56
+ ok: true,
57
+ json: async () => ({ success: true, data: { results: [], total: 0, searchType: 'hybrid' }, creditsCharged: 1, creditsRemaining: 99 }),
58
+ });
59
+ await client.query({ query: 'test', searchType: 'hybrid', limit: 5 });
60
+ const [url, options] = mockFetch.mock.calls[0];
61
+ expect(url).toBe('http://localhost:5050/api/v1/agent/query');
62
+ expect(options.method).toBe('POST');
63
+ expect(JSON.parse(options.body)).toEqual({ query: 'test', searchType: 'hybrid', limit: 5 });
64
+ });
65
+ });
66
+ describe('endpoint methods', () => {
67
+ const mockOkResponse = (data) => ({
68
+ ok: true,
69
+ json: async () => ({ success: true, data, creditsCharged: 1, creditsRemaining: 99 }),
70
+ });
71
+ it('getSchema calls GET /api/v1/agent/schema', async () => {
72
+ mockFetch.mockResolvedValueOnce(mockOkResponse({ contentTypes: {}, tags: [], capabilities: [], dateRange: { earliest: null, latest: null } }));
73
+ await client.getSchema();
74
+ expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:5050/api/v1/agent/schema');
75
+ expect(mockFetch.mock.calls[0][1].method).toBe('GET');
76
+ });
77
+ it('scrape calls POST /api/v1/research/scrape', async () => {
78
+ mockFetch.mockResolvedValueOnce(mockOkResponse({ id: '1', url: 'https://example.com', title: 'Test', content: 'text' }));
79
+ await client.scrape('https://example.com', { includeHtml: true });
80
+ expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:5050/api/v1/research/scrape');
81
+ expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toEqual({ url: 'https://example.com', options: { includeHtml: true } });
82
+ });
83
+ it('webSearch calls POST /api/v1/research/search', async () => {
84
+ mockFetch.mockResolvedValueOnce(mockOkResponse({ results: [], query: 'test' }));
85
+ await client.webSearch('test', { site: 'example.com' });
86
+ expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:5050/api/v1/research/search');
87
+ });
88
+ });
89
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { handleSearchContent, handleGetContent, handleListTags } from '../tools.js';
3
+ import { ApiError } from '../api-client.js';
4
+ // Create a mock client
5
+ function createMockClient(overrides = {}) {
6
+ return {
7
+ getSchema: vi.fn(),
8
+ query: vi.fn(),
9
+ retrieve: vi.fn(),
10
+ summarize: vi.fn(),
11
+ scrape: vi.fn(),
12
+ crawl: vi.fn(),
13
+ webSearch: vi.fn(),
14
+ ...overrides,
15
+ };
16
+ }
17
+ describe('MCP Tool Handlers', () => {
18
+ describe('handleSearchContent', () => {
19
+ it('returns formatted results', async () => {
20
+ const client = createMockClient({
21
+ query: vi.fn().mockResolvedValue({
22
+ data: {
23
+ results: [
24
+ {
25
+ id: '1',
26
+ title: 'Test',
27
+ contentType: 'page',
28
+ relevanceScore: 0.95,
29
+ tags: [{ label: 'Tag1', slug: 'tag1', category: null }],
30
+ summary: null,
31
+ createdAt: '2024-01-01',
32
+ },
33
+ ],
34
+ total: 1,
35
+ searchType: 'hybrid',
36
+ },
37
+ creditsCharged: 1,
38
+ creditsRemaining: 99,
39
+ }),
40
+ });
41
+ const result = await handleSearchContent(client, {
42
+ query: 'test',
43
+ searchType: 'hybrid',
44
+ limit: 10,
45
+ include: ['summary', 'tags'],
46
+ });
47
+ expect(result).toContain('Search Results');
48
+ expect(result).toContain('Test');
49
+ expect(result).toContain('0.950');
50
+ expect(result).toContain('Credits charged: 1');
51
+ });
52
+ it('returns no results message', async () => {
53
+ const client = createMockClient({
54
+ query: vi.fn().mockResolvedValue({
55
+ data: { results: [], total: 0, searchType: 'hybrid' },
56
+ creditsCharged: 1,
57
+ creditsRemaining: 99,
58
+ }),
59
+ });
60
+ const result = await handleSearchContent(client, {
61
+ query: 'nothing',
62
+ searchType: 'hybrid',
63
+ limit: 10,
64
+ include: ['summary'],
65
+ });
66
+ expect(result).toContain('No results found');
67
+ });
68
+ it('propagates ApiError', async () => {
69
+ const client = createMockClient({
70
+ query: vi.fn().mockRejectedValue(new ApiError(401, 'INVALID_API_KEY', 'Bad key')),
71
+ });
72
+ await expect(handleSearchContent(client, { query: 'test', searchType: 'hybrid', limit: 10, include: [] })).rejects.toThrow(ApiError);
73
+ });
74
+ it('includes tags in output when present', async () => {
75
+ const client = createMockClient({
76
+ query: vi.fn().mockResolvedValue({
77
+ data: {
78
+ results: [
79
+ {
80
+ id: '2',
81
+ title: 'Tagged Article',
82
+ contentType: 'article',
83
+ relevanceScore: 0.8,
84
+ tags: [
85
+ { label: 'Engineering', slug: 'engineering', category: 'dept' },
86
+ { label: 'Backend', slug: 'backend', category: 'dept' },
87
+ ],
88
+ summary: 'A great article',
89
+ createdAt: '2024-06-01',
90
+ },
91
+ ],
92
+ total: 1,
93
+ searchType: 'fulltext',
94
+ },
95
+ creditsCharged: 1,
96
+ creditsRemaining: 50,
97
+ }),
98
+ });
99
+ const result = await handleSearchContent(client, {
100
+ query: 'engineering',
101
+ searchType: 'fulltext',
102
+ limit: 5,
103
+ include: ['summary', 'tags'],
104
+ });
105
+ expect(result).toContain('Engineering');
106
+ expect(result).toContain('Backend');
107
+ expect(result).toContain('A great article');
108
+ });
109
+ });
110
+ describe('handleGetContent', () => {
111
+ it('returns formatted items', async () => {
112
+ const client = createMockClient({
113
+ retrieve: vi.fn().mockResolvedValue({
114
+ data: {
115
+ items: [
116
+ {
117
+ id: 'abc-123',
118
+ title: 'My Page',
119
+ contentType: 'page',
120
+ createdAt: '2024-01-01',
121
+ updatedAt: '2024-01-02',
122
+ sourceUrl: 'https://example.com/page',
123
+ summary: 'A summary',
124
+ tags: [],
125
+ content: 'Full content here',
126
+ children: [],
127
+ },
128
+ ],
129
+ },
130
+ creditsCharged: 1,
131
+ creditsRemaining: 98,
132
+ }),
133
+ });
134
+ const result = await handleGetContent(client, { ids: ['abc-123'], include: ['content', 'summary'] });
135
+ expect(result).toContain('My Page');
136
+ expect(result).toContain('https://example.com/page');
137
+ expect(result).toContain('Full content here');
138
+ expect(result).toContain('Credits charged: 1');
139
+ });
140
+ it('returns no items message when empty', async () => {
141
+ const client = createMockClient({
142
+ retrieve: vi.fn().mockResolvedValue({
143
+ data: { items: [] },
144
+ creditsCharged: 1,
145
+ creditsRemaining: 97,
146
+ }),
147
+ });
148
+ const result = await handleGetContent(client, { ids: ['missing-id'], include: ['summary'] });
149
+ expect(result).toContain('No items found');
150
+ });
151
+ it('propagates ApiError', async () => {
152
+ const client = createMockClient({
153
+ retrieve: vi.fn().mockRejectedValue(new ApiError(402, 'INSUFFICIENT_CREDITS', 'No credits')),
154
+ });
155
+ await expect(handleGetContent(client, { ids: ['id-1'], include: ['content'] })).rejects.toThrow(ApiError);
156
+ });
157
+ });
158
+ describe('handleListTags', () => {
159
+ it('groups tags by category', async () => {
160
+ const client = createMockClient({
161
+ getSchema: vi.fn().mockResolvedValue({
162
+ data: {
163
+ contentTypes: { page: 5, article: 3 },
164
+ tags: [
165
+ { slug: 'a', label: 'A', category: 'cat1', description: 'desc' },
166
+ { slug: 'b', label: 'B', category: 'cat1', description: null },
167
+ { slug: 'c', label: 'C', category: null, description: null },
168
+ ],
169
+ capabilities: ['search', 'retrieve'],
170
+ dateRange: { earliest: '2024-01-01', latest: '2024-12-31' },
171
+ },
172
+ creditsCharged: 0,
173
+ creditsRemaining: 100,
174
+ }),
175
+ });
176
+ const result = await handleListTags(client);
177
+ expect(result).toContain('cat1');
178
+ expect(result).toContain('uncategorized');
179
+ expect(result).toContain('page');
180
+ expect(result).toContain('article');
181
+ expect(result).toContain('search, retrieve');
182
+ expect(result).toContain('2024-01-01');
183
+ });
184
+ it('includes tag descriptions when present', async () => {
185
+ const client = createMockClient({
186
+ getSchema: vi.fn().mockResolvedValue({
187
+ data: {
188
+ contentTypes: {},
189
+ tags: [
190
+ { slug: 'tech', label: 'Technology', category: 'topic', description: 'Tech articles' },
191
+ ],
192
+ capabilities: [],
193
+ dateRange: { earliest: null, latest: null },
194
+ },
195
+ creditsCharged: 0,
196
+ creditsRemaining: 100,
197
+ }),
198
+ });
199
+ const result = await handleListTags(client);
200
+ expect(result).toContain('Tech articles');
201
+ expect(result).toContain('Technology');
202
+ });
203
+ it('propagates ApiError', async () => {
204
+ const client = createMockClient({
205
+ getSchema: vi.fn().mockRejectedValue(new ApiError(429, 'RATE_LIMIT', 'Too many requests')),
206
+ });
207
+ await expect(handleListTags(client)).rejects.toThrow(ApiError);
208
+ });
209
+ });
210
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Answer Engine API Client
3
+ * HTTP wrapper for agent query and research endpoints
4
+ */
5
+ interface ApiResponse<T> {
6
+ success: boolean;
7
+ data: T;
8
+ creditsCharged: number;
9
+ creditsRemaining: number;
10
+ }
11
+ export interface ApiClientConfig {
12
+ apiUrl: string;
13
+ apiKey: string;
14
+ }
15
+ export declare class AnswerEngineClient {
16
+ private apiUrl;
17
+ private apiKey;
18
+ constructor(config: ApiClientConfig);
19
+ private request;
20
+ getSchema(): Promise<ApiResponse<SchemaResponse>>;
21
+ query(input: QueryInput): Promise<ApiResponse<QueryResult>>;
22
+ retrieve(input: RetrieveInput): Promise<ApiResponse<RetrieveResult>>;
23
+ summarize(input: SummarizeInput): Promise<ApiResponse<SummarizeResult>>;
24
+ scrape(url: string, options?: {
25
+ includeHtml?: boolean;
26
+ }): Promise<ApiResponse<ScrapeResult>>;
27
+ crawl(domain: string, maxPages?: number): Promise<ApiResponse<CrawlResult>>;
28
+ webSearch(query: string, options?: {
29
+ site?: string;
30
+ scrapeResults?: boolean;
31
+ }): Promise<ApiResponse<WebSearchResult>>;
32
+ }
33
+ export declare class ApiError extends Error {
34
+ readonly statusCode: number;
35
+ readonly code: string;
36
+ constructor(statusCode: number, code: string, message: string);
37
+ }
38
+ export interface SchemaResponse {
39
+ contentTypes: Record<string, number>;
40
+ tags: Array<{
41
+ slug: string;
42
+ label: string;
43
+ description: string | null;
44
+ category: string | null;
45
+ }>;
46
+ capabilities: string[];
47
+ dateRange: {
48
+ earliest: string | null;
49
+ latest: string | null;
50
+ };
51
+ }
52
+ export interface QueryInput {
53
+ query: string;
54
+ searchType?: 'fulltext' | 'semantic' | 'hybrid';
55
+ filters?: {
56
+ contentTypes?: string[];
57
+ tags?: string[];
58
+ dateFrom?: string;
59
+ dateTo?: string;
60
+ status?: 'active' | 'archived';
61
+ };
62
+ include?: Array<'summary' | 'content' | 'tags' | 'metadata'>;
63
+ limit?: number;
64
+ }
65
+ export interface QueryResultItem {
66
+ id: string;
67
+ contentType: string;
68
+ title: string;
69
+ summary?: string | null;
70
+ content?: string | null;
71
+ tags?: Array<{
72
+ slug: string;
73
+ label: string;
74
+ category: string | null;
75
+ }>;
76
+ metadata?: Record<string, unknown>;
77
+ relevanceScore: number;
78
+ createdAt: string;
79
+ }
80
+ export interface QueryResult {
81
+ results: QueryResultItem[];
82
+ total: number;
83
+ searchType: string;
84
+ }
85
+ export interface RetrieveInput {
86
+ ids: string[];
87
+ include?: Array<'summary' | 'content' | 'tags' | 'metadata' | 'children' | 'analysis'>;
88
+ }
89
+ export interface RetrieveResultItem {
90
+ id: string;
91
+ contentType: string;
92
+ title: string;
93
+ summary?: string | null;
94
+ content?: string | null;
95
+ tags?: Array<{
96
+ slug: string;
97
+ label: string;
98
+ category: string | null;
99
+ }>;
100
+ metadata?: Record<string, unknown>;
101
+ analysis?: Record<string, unknown>;
102
+ children?: Array<{
103
+ id: string;
104
+ title: string;
105
+ contentType: string;
106
+ }>;
107
+ sourceUrl: string | null;
108
+ createdAt: string;
109
+ updatedAt: string;
110
+ }
111
+ export interface RetrieveResult {
112
+ items: RetrieveResultItem[];
113
+ }
114
+ export interface SummarizeInput {
115
+ prompt: string;
116
+ filter?: {
117
+ contentTypes?: string[];
118
+ tags?: string[];
119
+ dateFrom?: string;
120
+ dateTo?: string;
121
+ };
122
+ limit?: number;
123
+ }
124
+ export interface SummarizeResult {
125
+ summary: string;
126
+ sourceCount: number;
127
+ prompt: string;
128
+ }
129
+ export interface ScrapeResult {
130
+ id: string;
131
+ url: string;
132
+ title: string;
133
+ content: string;
134
+ html?: string;
135
+ }
136
+ export interface CrawlResult {
137
+ pages: Array<{
138
+ url: string;
139
+ title: string;
140
+ status: string;
141
+ }>;
142
+ totalPages: number;
143
+ }
144
+ export interface WebSearchResult {
145
+ results: Array<{
146
+ title: string;
147
+ url: string;
148
+ snippet: string;
149
+ content?: string;
150
+ }>;
151
+ query: string;
152
+ }
153
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Answer Engine API Client
3
+ * HTTP wrapper for agent query and research endpoints
4
+ */
5
+ export class AnswerEngineClient {
6
+ apiUrl;
7
+ apiKey;
8
+ constructor(config) {
9
+ this.apiUrl = config.apiUrl.replace(/\/+$/, '');
10
+ this.apiKey = config.apiKey;
11
+ }
12
+ async request(method, path, body) {
13
+ const headers = {
14
+ 'Content-Type': 'application/json',
15
+ 'X-API-Key': this.apiKey,
16
+ };
17
+ const options = { method, headers };
18
+ if (body)
19
+ options.body = JSON.stringify(body);
20
+ const response = await fetch(`${this.apiUrl}${path}`, options);
21
+ const data = (await response.json());
22
+ if (!response.ok) {
23
+ const errorData = data;
24
+ const code = errorData.error?.code ?? `HTTP_${response.status}`;
25
+ const message = errorData.error?.message ?? `Request failed with status ${response.status}`;
26
+ throw new ApiError(response.status, code, message);
27
+ }
28
+ return data;
29
+ }
30
+ // Agent endpoints
31
+ async getSchema() {
32
+ return this.request('GET', '/api/v1/agent/schema');
33
+ }
34
+ async query(input) {
35
+ return this.request('POST', '/api/v1/agent/query', input);
36
+ }
37
+ async retrieve(input) {
38
+ return this.request('POST', '/api/v1/agent/retrieve', input);
39
+ }
40
+ async summarize(input) {
41
+ return this.request('POST', '/api/v1/agent/summarize', input);
42
+ }
43
+ // Research endpoints
44
+ async scrape(url, options) {
45
+ return this.request('POST', '/api/v1/research/scrape', { url, options });
46
+ }
47
+ async crawl(domain, maxPages) {
48
+ return this.request('POST', '/api/v1/research/crawl', { domain, maxPages });
49
+ }
50
+ async webSearch(query, options) {
51
+ return this.request('POST', '/api/v1/research/search', { query, options });
52
+ }
53
+ }
54
+ export class ApiError extends Error {
55
+ statusCode;
56
+ code;
57
+ constructor(statusCode, code, message) {
58
+ super(message);
59
+ this.statusCode = statusCode;
60
+ this.code = code;
61
+ this.name = 'ApiError';
62
+ }
63
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Answer Engine MCP Server
4
+ * Provides Answer Engine search, retrieval, and research capabilities via MCP stdio transport
5
+ */
6
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Answer Engine MCP Server
4
+ * Provides Answer Engine search, retrieval, and research capabilities via MCP stdio transport
5
+ */
6
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { z } from 'zod';
9
+ import { AnswerEngineClient, ApiError } from './api-client.js';
10
+ import { SearchContentSchema, GetContentSchema, SummarizeCollectionSchema, ScrapeUrlSchema, CrawlDomainSchema, WebSearchSchema, handleSearchContent, handleGetContent, handleListTags, handleSummarizeCollection, handleScrapeUrl, handleCrawlDomain, handleWebSearch, } from './tools.js';
11
+ import { staticResources, resourceTemplateConfigs, readResource } from './resources.js';
12
+ // Configuration from environment
13
+ const API_URL = process.env.ANSWER_ENGINE_API_URL ?? 'http://localhost:5050';
14
+ const API_KEY = process.env.ANSWER_ENGINE_API_KEY ?? '';
15
+ if (!API_KEY) {
16
+ process.stderr.write('Warning: ANSWER_ENGINE_API_KEY not set. API calls will fail.\n');
17
+ }
18
+ // Create API client
19
+ const client = new AnswerEngineClient({ apiUrl: API_URL, apiKey: API_KEY });
20
+ // Format errors for MCP error responses
21
+ function formatMcpError(error) {
22
+ if (error instanceof ApiError) {
23
+ if (error.statusCode === 401)
24
+ return 'Authentication failed. Check your ANSWER_ENGINE_API_KEY.';
25
+ if (error.statusCode === 402)
26
+ return 'Insufficient credits. Please add more credits to your account.';
27
+ if (error.statusCode === 429)
28
+ return 'Rate limit exceeded. Please wait before making more requests.';
29
+ return `API error (${error.code}): ${error.message}`;
30
+ }
31
+ if (error instanceof z.ZodError) {
32
+ return `Invalid input: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`;
33
+ }
34
+ return `Unexpected error: ${String(error)}`;
35
+ }
36
+ // Create MCP server
37
+ const server = new McpServer({
38
+ name: 'answer-engine',
39
+ version: '1.0.0',
40
+ });
41
+ // -----------------------------------------------------------------------
42
+ // Register tools — pass .shape (ZodRawShape), not the ZodObject itself
43
+ // -----------------------------------------------------------------------
44
+ server.tool('search_content', 'Search your Answer Engine content library using fulltext, semantic, or hybrid search. Returns matching content items with relevance scores. Costs 1 credit per search.', SearchContentSchema.shape, async (params) => {
45
+ try {
46
+ const text = await handleSearchContent(client, params);
47
+ return { content: [{ type: 'text', text }] };
48
+ }
49
+ catch (error) {
50
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
51
+ }
52
+ });
53
+ server.tool('get_content', 'Retrieve specific content items by their IDs. Returns full content details including text, tags, metadata, and children. Costs 1 credit.', GetContentSchema.shape, async (params) => {
54
+ try {
55
+ const text = await handleGetContent(client, params);
56
+ return { content: [{ type: 'text', text }] };
57
+ }
58
+ catch (error) {
59
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
60
+ }
61
+ });
62
+ server.tool('list_tags', 'List all available tags and content types in your Answer Engine library. Useful for discovering what content is available and filtering searches. Free (0 credits).', {}, async () => {
63
+ try {
64
+ const text = await handleListTags(client);
65
+ return { content: [{ type: 'text', text }] };
66
+ }
67
+ catch (error) {
68
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
69
+ }
70
+ });
71
+ server.tool('summarize_collection', 'Use AI to summarize or analyze content from your Answer Engine library. Provide a prompt describing what you want to know, and optionally filter by content type, tags, or dates. Costs 10 credits.', SummarizeCollectionSchema.shape, async (params) => {
72
+ try {
73
+ const text = await handleSummarizeCollection(client, params);
74
+ return { content: [{ type: 'text', text }] };
75
+ }
76
+ catch (error) {
77
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
78
+ }
79
+ });
80
+ server.tool('scrape_url', 'Scrape a web page and save it to your Answer Engine content library. Returns the extracted title and content. Costs 1 credit.', ScrapeUrlSchema.shape, async (params) => {
81
+ try {
82
+ const text = await handleScrapeUrl(client, params);
83
+ return { content: [{ type: 'text', text }] };
84
+ }
85
+ catch (error) {
86
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
87
+ }
88
+ });
89
+ server.tool('crawl_domain', 'Crawl multiple pages from a domain and save them to your Answer Engine library. Costs 1 credit per page crawled.', CrawlDomainSchema.shape, async (params) => {
90
+ try {
91
+ const text = await handleCrawlDomain(client, params);
92
+ return { content: [{ type: 'text', text }] };
93
+ }
94
+ catch (error) {
95
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
96
+ }
97
+ });
98
+ server.tool('web_search', 'Search the web and optionally scrape results. Use the site parameter to limit to a specific domain. Costs 1 credit.', WebSearchSchema.shape, async (params) => {
99
+ try {
100
+ const text = await handleWebSearch(client, params);
101
+ return { content: [{ type: 'text', text }] };
102
+ }
103
+ catch (error) {
104
+ return { content: [{ type: 'text', text: formatMcpError(error) }], isError: true };
105
+ }
106
+ });
107
+ // -----------------------------------------------------------------------
108
+ // Register static resources
109
+ // -----------------------------------------------------------------------
110
+ for (const resource of staticResources) {
111
+ server.resource(resource.name, resource.uri, { description: resource.description, mimeType: resource.mimeType }, async (uri) => {
112
+ return readResource(client, uri.href);
113
+ });
114
+ }
115
+ // -----------------------------------------------------------------------
116
+ // Register resource templates
117
+ // -----------------------------------------------------------------------
118
+ for (const templateConfig of resourceTemplateConfigs) {
119
+ const template = new ResourceTemplate(templateConfig.uriTemplate, { list: undefined });
120
+ server.resource(templateConfig.name, template, { description: templateConfig.description, mimeType: templateConfig.mimeType }, async (uri) => {
121
+ return readResource(client, uri.href);
122
+ });
123
+ }
124
+ // -----------------------------------------------------------------------
125
+ // Start server
126
+ // -----------------------------------------------------------------------
127
+ async function main() {
128
+ const transport = new StdioServerTransport();
129
+ await server.connect(transport);
130
+ process.stderr.write('Answer Engine MCP server started\n');
131
+ }
132
+ main().catch((error) => {
133
+ process.stderr.write(`Fatal error: ${String(error)}\n`);
134
+ process.exit(1);
135
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MCP Resource Definitions
3
+ * Expose Answer Engine data as MCP resources
4
+ */
5
+ import { AnswerEngineClient } from './api-client.js';
6
+ export interface ResourceMetadata {
7
+ uri: string;
8
+ name: string;
9
+ description: string;
10
+ mimeType: string;
11
+ }
12
+ export declare const staticResources: ResourceMetadata[];
13
+ export declare const resourceTemplateConfigs: {
14
+ uriTemplate: string;
15
+ name: string;
16
+ description: string;
17
+ mimeType: string;
18
+ }[];
19
+ export declare function readResource(client: AnswerEngineClient, uri: string): Promise<{
20
+ contents: Array<{
21
+ uri: string;
22
+ mimeType: string;
23
+ text: string;
24
+ }>;
25
+ }>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * MCP Resource Definitions
3
+ * Expose Answer Engine data as MCP resources
4
+ */
5
+ import { ApiError } from './api-client.js';
6
+ export const staticResources = [
7
+ {
8
+ uri: 'answer-engine://schema',
9
+ name: 'Content Schema',
10
+ description: 'Available content types, tags, and capabilities in your Answer Engine library',
11
+ mimeType: 'application/json',
12
+ },
13
+ ];
14
+ export const resourceTemplateConfigs = [
15
+ {
16
+ uriTemplate: 'answer-engine://content/{id}',
17
+ name: 'Content Item',
18
+ description: 'Retrieve a specific content item by ID',
19
+ mimeType: 'application/json',
20
+ },
21
+ ];
22
+ export async function readResource(client, uri) {
23
+ try {
24
+ if (uri === 'answer-engine://schema') {
25
+ const response = await client.getSchema();
26
+ return {
27
+ contents: [
28
+ {
29
+ uri,
30
+ mimeType: 'application/json',
31
+ text: JSON.stringify(response.data, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ const contentMatch = /^answer-engine:\/\/content\/(.+)$/.exec(uri);
37
+ if (contentMatch) {
38
+ const id = contentMatch[1];
39
+ const response = await client.retrieve({
40
+ ids: [id],
41
+ include: ['summary', 'content', 'tags', 'metadata', 'children'],
42
+ });
43
+ const item = response.data.items[0];
44
+ if (!item) {
45
+ throw new Error(`Content item not found: ${id}`);
46
+ }
47
+ return {
48
+ contents: [
49
+ {
50
+ uri,
51
+ mimeType: 'application/json',
52
+ text: JSON.stringify(item, null, 2),
53
+ },
54
+ ],
55
+ };
56
+ }
57
+ throw new Error(`Unknown resource URI: ${uri}`);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof ApiError) {
61
+ throw new Error(`API error (${error.code}): ${error.message}`);
62
+ }
63
+ throw error;
64
+ }
65
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * MCP Tool Definitions
3
+ * Maps Answer Engine API endpoints to MCP tools with Zod input schemas
4
+ */
5
+ import { z } from 'zod';
6
+ import { AnswerEngineClient } from './api-client.js';
7
+ export declare const SearchContentSchema: z.ZodObject<{
8
+ query: z.ZodString;
9
+ searchType: z.ZodDefault<z.ZodEnum<["fulltext", "semantic", "hybrid"]>>;
10
+ filters: z.ZodOptional<z.ZodObject<{
11
+ contentTypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
12
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ dateFrom: z.ZodOptional<z.ZodString>;
14
+ dateTo: z.ZodOptional<z.ZodString>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ tags?: string[] | undefined;
17
+ contentTypes?: string[] | undefined;
18
+ dateFrom?: string | undefined;
19
+ dateTo?: string | undefined;
20
+ }, {
21
+ tags?: string[] | undefined;
22
+ contentTypes?: string[] | undefined;
23
+ dateFrom?: string | undefined;
24
+ dateTo?: string | undefined;
25
+ }>>;
26
+ limit: z.ZodDefault<z.ZodNumber>;
27
+ include: z.ZodDefault<z.ZodArray<z.ZodEnum<["summary", "content", "tags", "metadata"]>, "many">>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ query: string;
30
+ searchType: "fulltext" | "semantic" | "hybrid";
31
+ limit: number;
32
+ include: ("summary" | "content" | "tags" | "metadata")[];
33
+ filters?: {
34
+ tags?: string[] | undefined;
35
+ contentTypes?: string[] | undefined;
36
+ dateFrom?: string | undefined;
37
+ dateTo?: string | undefined;
38
+ } | undefined;
39
+ }, {
40
+ query: string;
41
+ searchType?: "fulltext" | "semantic" | "hybrid" | undefined;
42
+ filters?: {
43
+ tags?: string[] | undefined;
44
+ contentTypes?: string[] | undefined;
45
+ dateFrom?: string | undefined;
46
+ dateTo?: string | undefined;
47
+ } | undefined;
48
+ limit?: number | undefined;
49
+ include?: ("summary" | "content" | "tags" | "metadata")[] | undefined;
50
+ }>;
51
+ export declare const GetContentSchema: z.ZodObject<{
52
+ ids: z.ZodArray<z.ZodString, "many">;
53
+ include: z.ZodDefault<z.ZodArray<z.ZodEnum<["summary", "content", "tags", "metadata", "children", "analysis"]>, "many">>;
54
+ }, "strip", z.ZodTypeAny, {
55
+ include: ("summary" | "content" | "tags" | "metadata" | "children" | "analysis")[];
56
+ ids: string[];
57
+ }, {
58
+ ids: string[];
59
+ include?: ("summary" | "content" | "tags" | "metadata" | "children" | "analysis")[] | undefined;
60
+ }>;
61
+ export declare const SummarizeCollectionSchema: z.ZodObject<{
62
+ prompt: z.ZodString;
63
+ filter: z.ZodOptional<z.ZodObject<{
64
+ contentTypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
65
+ tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
66
+ dateFrom: z.ZodOptional<z.ZodString>;
67
+ dateTo: z.ZodOptional<z.ZodString>;
68
+ }, "strip", z.ZodTypeAny, {
69
+ tags?: string[] | undefined;
70
+ contentTypes?: string[] | undefined;
71
+ dateFrom?: string | undefined;
72
+ dateTo?: string | undefined;
73
+ }, {
74
+ tags?: string[] | undefined;
75
+ contentTypes?: string[] | undefined;
76
+ dateFrom?: string | undefined;
77
+ dateTo?: string | undefined;
78
+ }>>;
79
+ limit: z.ZodDefault<z.ZodNumber>;
80
+ }, "strip", z.ZodTypeAny, {
81
+ limit: number;
82
+ prompt: string;
83
+ filter?: {
84
+ tags?: string[] | undefined;
85
+ contentTypes?: string[] | undefined;
86
+ dateFrom?: string | undefined;
87
+ dateTo?: string | undefined;
88
+ } | undefined;
89
+ }, {
90
+ prompt: string;
91
+ filter?: {
92
+ tags?: string[] | undefined;
93
+ contentTypes?: string[] | undefined;
94
+ dateFrom?: string | undefined;
95
+ dateTo?: string | undefined;
96
+ } | undefined;
97
+ limit?: number | undefined;
98
+ }>;
99
+ export declare const ScrapeUrlSchema: z.ZodObject<{
100
+ url: z.ZodString;
101
+ includeHtml: z.ZodDefault<z.ZodBoolean>;
102
+ }, "strip", z.ZodTypeAny, {
103
+ url: string;
104
+ includeHtml: boolean;
105
+ }, {
106
+ url: string;
107
+ includeHtml?: boolean | undefined;
108
+ }>;
109
+ export declare const CrawlDomainSchema: z.ZodObject<{
110
+ domain: z.ZodString;
111
+ maxPages: z.ZodDefault<z.ZodNumber>;
112
+ }, "strip", z.ZodTypeAny, {
113
+ domain: string;
114
+ maxPages: number;
115
+ }, {
116
+ domain: string;
117
+ maxPages?: number | undefined;
118
+ }>;
119
+ export declare const WebSearchSchema: z.ZodObject<{
120
+ query: z.ZodString;
121
+ site: z.ZodOptional<z.ZodString>;
122
+ scrapeResults: z.ZodDefault<z.ZodBoolean>;
123
+ }, "strip", z.ZodTypeAny, {
124
+ query: string;
125
+ scrapeResults: boolean;
126
+ site?: string | undefined;
127
+ }, {
128
+ query: string;
129
+ site?: string | undefined;
130
+ scrapeResults?: boolean | undefined;
131
+ }>;
132
+ export declare function handleSearchContent(client: AnswerEngineClient, input: z.infer<typeof SearchContentSchema>): Promise<string>;
133
+ export declare function handleGetContent(client: AnswerEngineClient, input: z.infer<typeof GetContentSchema>): Promise<string>;
134
+ export declare function handleListTags(client: AnswerEngineClient): Promise<string>;
135
+ export declare function handleSummarizeCollection(client: AnswerEngineClient, input: z.infer<typeof SummarizeCollectionSchema>): Promise<string>;
136
+ export declare function handleScrapeUrl(client: AnswerEngineClient, input: z.infer<typeof ScrapeUrlSchema>): Promise<string>;
137
+ export declare function handleCrawlDomain(client: AnswerEngineClient, input: z.infer<typeof CrawlDomainSchema>): Promise<string>;
138
+ export declare function handleWebSearch(client: AnswerEngineClient, input: z.infer<typeof WebSearchSchema>): Promise<string>;
package/dist/tools.js ADDED
@@ -0,0 +1,223 @@
1
+ /**
2
+ * MCP Tool Definitions
3
+ * Maps Answer Engine API endpoints to MCP tools with Zod input schemas
4
+ */
5
+ import { z } from 'zod';
6
+ // -----------------------------------------------------------------------
7
+ // Input schemas (ZodObject so we can expose .shape to the MCP SDK)
8
+ // -----------------------------------------------------------------------
9
+ export const SearchContentSchema = z.object({
10
+ query: z.string().describe('Search query text'),
11
+ searchType: z
12
+ .enum(['fulltext', 'semantic', 'hybrid'])
13
+ .default('hybrid')
14
+ .describe('Search algorithm to use'),
15
+ filters: z
16
+ .object({
17
+ contentTypes: z.array(z.string()).optional().describe('Filter by content types'),
18
+ tags: z.array(z.string()).optional().describe('Filter by tag slugs'),
19
+ dateFrom: z.string().optional().describe('Filter: created after this ISO date'),
20
+ dateTo: z.string().optional().describe('Filter: created before this ISO date'),
21
+ })
22
+ .optional()
23
+ .describe('Optional filters to narrow results'),
24
+ limit: z
25
+ .number()
26
+ .int()
27
+ .min(1)
28
+ .max(50)
29
+ .default(10)
30
+ .describe('Maximum number of results'),
31
+ include: z
32
+ .array(z.enum(['summary', 'content', 'tags', 'metadata']))
33
+ .default(['summary', 'tags'])
34
+ .describe('Fields to include in results'),
35
+ });
36
+ export const GetContentSchema = z.object({
37
+ ids: z.array(z.string()).min(1).max(50).describe('Content item UUIDs to retrieve'),
38
+ include: z
39
+ .array(z.enum(['summary', 'content', 'tags', 'metadata', 'children', 'analysis']))
40
+ .default(['summary', 'content', 'tags'])
41
+ .describe('Fields to include'),
42
+ });
43
+ export const SummarizeCollectionSchema = z.object({
44
+ prompt: z.string().describe('What to summarize or analyze about the content'),
45
+ filter: z
46
+ .object({
47
+ contentTypes: z.array(z.string()).optional(),
48
+ tags: z.array(z.string()).optional(),
49
+ dateFrom: z.string().optional(),
50
+ dateTo: z.string().optional(),
51
+ })
52
+ .optional()
53
+ .describe('Filter which content to include in the summary'),
54
+ limit: z
55
+ .number()
56
+ .int()
57
+ .min(1)
58
+ .max(100)
59
+ .default(20)
60
+ .describe('Max content items to analyze'),
61
+ });
62
+ export const ScrapeUrlSchema = z.object({
63
+ url: z.string().url().describe('URL to scrape'),
64
+ includeHtml: z.boolean().default(false).describe('Include raw HTML in response'),
65
+ });
66
+ export const CrawlDomainSchema = z.object({
67
+ domain: z.string().describe('Domain to crawl (e.g., example.com)'),
68
+ maxPages: z
69
+ .number()
70
+ .int()
71
+ .min(1)
72
+ .max(100)
73
+ .default(10)
74
+ .describe('Maximum pages to crawl (1 credit per page)'),
75
+ });
76
+ export const WebSearchSchema = z.object({
77
+ query: z.string().describe('Search query'),
78
+ site: z.string().optional().describe('Limit search to a specific site'),
79
+ scrapeResults: z
80
+ .boolean()
81
+ .default(false)
82
+ .describe('Scrape full content from search result URLs'),
83
+ });
84
+ // -----------------------------------------------------------------------
85
+ // Helpers
86
+ // -----------------------------------------------------------------------
87
+ function formatCredits(creditsCharged, creditsRemaining) {
88
+ return `\n\n---\nCredits charged: ${creditsCharged} | Credits remaining: ${creditsRemaining}`;
89
+ }
90
+ // -----------------------------------------------------------------------
91
+ // Tool handlers
92
+ // -----------------------------------------------------------------------
93
+ export async function handleSearchContent(client, input) {
94
+ const response = await client.query(input);
95
+ const { results, total, searchType } = response.data;
96
+ if (results.length === 0) {
97
+ return `No results found for "${input.query}" (${searchType} search).${formatCredits(response.creditsCharged, response.creditsRemaining)}`;
98
+ }
99
+ let output = `## Search Results (${total} total, showing ${results.length}, ${searchType})\n\n`;
100
+ for (const item of results) {
101
+ output += `### ${item.title}\n`;
102
+ output += `- **ID:** ${item.id}\n`;
103
+ output += `- **Type:** ${item.contentType}\n`;
104
+ output += `- **Relevance:** ${item.relevanceScore.toFixed(3)}\n`;
105
+ if (item.summary)
106
+ output += `- **Summary:** ${item.summary}\n`;
107
+ if (item.tags?.length)
108
+ output += `- **Tags:** ${item.tags.map((t) => t.label).join(', ')}\n`;
109
+ output += '\n';
110
+ }
111
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
112
+ return output;
113
+ }
114
+ export async function handleGetContent(client, input) {
115
+ const response = await client.retrieve(input);
116
+ const { items } = response.data;
117
+ if (items.length === 0) {
118
+ return `No items found for the provided IDs.${formatCredits(response.creditsCharged, response.creditsRemaining)}`;
119
+ }
120
+ let output = `## Retrieved ${items.length} Item(s)\n\n`;
121
+ for (const item of items) {
122
+ output += `### ${item.title}\n`;
123
+ output += `- **ID:** ${item.id}\n`;
124
+ output += `- **Type:** ${item.contentType}\n`;
125
+ output += `- **Created:** ${item.createdAt}\n`;
126
+ if (item.sourceUrl)
127
+ output += `- **URL:** ${item.sourceUrl}\n`;
128
+ if (item.summary)
129
+ output += `- **Summary:** ${item.summary}\n`;
130
+ if (item.tags?.length)
131
+ output += `- **Tags:** ${item.tags.map((t) => t.label).join(', ')}\n`;
132
+ if (item.content)
133
+ output += `\n${item.content}\n`;
134
+ if (item.children?.length) {
135
+ output += `\n**Children (${item.children.length}):**\n`;
136
+ for (const child of item.children) {
137
+ output += ` - ${child.title} (${child.id})\n`;
138
+ }
139
+ }
140
+ output += '\n';
141
+ }
142
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
143
+ return output;
144
+ }
145
+ export async function handleListTags(client) {
146
+ const response = await client.getSchema();
147
+ const { contentTypes, tags, capabilities, dateRange } = response.data;
148
+ let output = '## Content Schema\n\n';
149
+ output += '### Content Types\n';
150
+ for (const [type, count] of Object.entries(contentTypes)) {
151
+ output += `- **${type}:** ${count} items\n`;
152
+ }
153
+ output += `\n### Tags (${tags.length} total)\n`;
154
+ const byCategory = {};
155
+ for (const tag of tags) {
156
+ const cat = tag.category ?? 'uncategorized';
157
+ if (!byCategory[cat])
158
+ byCategory[cat] = [];
159
+ byCategory[cat].push(tag);
160
+ }
161
+ for (const [category, categoryTags] of Object.entries(byCategory)) {
162
+ output += `\n**${category}:**\n`;
163
+ for (const tag of categoryTags) {
164
+ output += `- ${tag.label} (\`${tag.slug}\`)`;
165
+ if (tag.description)
166
+ output += ` — ${tag.description}`;
167
+ output += '\n';
168
+ }
169
+ }
170
+ output += `\n### Capabilities\n${capabilities.join(', ')}\n`;
171
+ output += `\n### Date Range\n${dateRange.earliest ?? 'n/a'} -> ${dateRange.latest ?? 'n/a'}\n`;
172
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
173
+ return output;
174
+ }
175
+ export async function handleSummarizeCollection(client, input) {
176
+ const response = await client.summarize(input);
177
+ const { summary, sourceCount } = response.data;
178
+ let output = `## Summary (${sourceCount} sources analyzed)\n\n`;
179
+ output += summary;
180
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
181
+ return output;
182
+ }
183
+ export async function handleScrapeUrl(client, input) {
184
+ const response = await client.scrape(input.url, { includeHtml: input.includeHtml });
185
+ const { title, url, content } = response.data;
186
+ let output = `## Scraped: ${title}\n`;
187
+ output += `**URL:** ${url}\n\n`;
188
+ output += content;
189
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
190
+ return output;
191
+ }
192
+ export async function handleCrawlDomain(client, input) {
193
+ const response = await client.crawl(input.domain, input.maxPages);
194
+ const { pages, totalPages } = response.data;
195
+ let output = `## Crawl Results: ${input.domain} (${totalPages} pages)\n\n`;
196
+ for (const page of pages) {
197
+ const icon = page.status === 'success' ? '+' : 'x';
198
+ output += `${icon} ${page.title} -- ${page.url}\n`;
199
+ }
200
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
201
+ return output;
202
+ }
203
+ export async function handleWebSearch(client, input) {
204
+ const response = await client.webSearch(input.query, {
205
+ site: input.site,
206
+ scrapeResults: input.scrapeResults,
207
+ });
208
+ const { results, query: searchQuery } = response.data;
209
+ if (results.length === 0) {
210
+ return `No web results found for "${searchQuery}".${formatCredits(response.creditsCharged, response.creditsRemaining)}`;
211
+ }
212
+ let output = `## Web Search: "${searchQuery}" (${results.length} results)\n\n`;
213
+ for (const result of results) {
214
+ output += `### ${result.title}\n`;
215
+ output += `**URL:** ${result.url}\n`;
216
+ output += `${result.snippet}\n`;
217
+ if (result.content)
218
+ output += `\n${result.content}\n`;
219
+ output += '\n';
220
+ }
221
+ output += formatCredits(response.creditsCharged, response.creditsRemaining);
222
+ return output;
223
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@answer-engine/mcp-server",
3
+ "version": "1.0.1",
4
+ "description": "MCP server for Answer Engine - search, retrieve, and research content",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "answer-engine-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*.js",
12
+ "dist/**/*.d.ts"
13
+ ],
14
+ "scripts": {
15
+ "build": "node --max-old-space-size=4096 node_modules/typescript/bin/tsc",
16
+ "dev": "node --max-old-space-size=4096 node_modules/typescript/bin/tsc --watch",
17
+ "start": "node dist/index.js",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/the-answerai/answer-engine.git",
24
+ "directory": "packages/mcp-server"
25
+ },
26
+ "keywords": [
27
+ "answer-engine",
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "search",
31
+ "ai"
32
+ ],
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.12.1",
39
+ "zod": "^3.22.4"
40
+ },
41
+ "devDependencies": {
42
+ "typescript": "^5.3.3",
43
+ "@types/node": "^20.10.5",
44
+ "vitest": "^1.0.4"
45
+ }
46
+ }