@immicore/noc 1.0.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.
package/Dockerfile ADDED
@@ -0,0 +1,21 @@
1
+ FROM node:20-alpine AS builder
2
+
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm install
6
+
7
+ COPY tsconfig.json ./
8
+ COPY src ./src
9
+ RUN npm run build
10
+
11
+ FROM node:20-alpine
12
+
13
+ WORKDIR /app
14
+ COPY package*.json ./
15
+ RUN npm install --production
16
+
17
+ COPY --from=builder /app/dist ./dist
18
+
19
+ EXPOSE 3108
20
+
21
+ CMD ["node", "dist/index.js", "--http", "--port", "3108"]
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod schema for a single prompt template, with arguments.
4
+ */
5
+ export const promptSchema = z.object({
6
+ name: z.string().describe('Prompt unique name.'),
7
+ description: z.string().optional().describe('Prompt description.'),
8
+ arguments: z.array(z.object({
9
+ name: z.string().describe('Argument name.'),
10
+ description: z.string().optional().describe('Argument description.'),
11
+ required: z.boolean().optional().describe('Is this argument required?'),
12
+ type: z.string().describe('Argument type (e.g., string, number, boolean, enum, etc.)'),
13
+ })).optional().describe('Prompt arguments.'),
14
+ });
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod schema for a single resource.
4
+ */
5
+ export const resourceSchema = z.object({
6
+ id: z.string(),
7
+ type: z.string().optional(),
8
+ label: z.string().optional(),
9
+ name: z.string().optional(),
10
+ description: z.string().optional(),
11
+ uri: z.string().optional(), // 允许 formbro://xxx 等自定义 scheme
12
+ mimeType: z.string().optional(),
13
+ meta: z.record(z.any()).optional(),
14
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Helper to create tool registration config from Tool interface
3
+ */
4
+ export function createToolConfig(tool) {
5
+ return {
6
+ title: tool.annotations?.title || tool.name,
7
+ description: tool.description,
8
+ inputSchema: tool.inputShape,
9
+ outputSchema: tool.outputSchema,
10
+ annotations: {
11
+ readOnlyHint: tool.annotations?.readOnlyHint ?? true,
12
+ destructiveHint: tool.annotations?.destructiveHint ?? false,
13
+ idempotentHint: tool.annotations?.idempotentHint ?? true,
14
+ openWorldHint: tool.annotations?.openWorldHint ?? true,
15
+ },
16
+ };
17
+ }
18
+ /**
19
+ * Get required environment variable or throw
20
+ */
21
+ export function requireEnv(name) {
22
+ const value = process.env[name];
23
+ if (!value) {
24
+ throw new Error(`Required environment variable ${name} is not set`);
25
+ }
26
+ return value;
27
+ }
28
+ /**
29
+ * Get optional environment variable with default
30
+ */
31
+ export function getEnv(name, defaultValue) {
32
+ return process.env[name] || defaultValue;
33
+ }
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
6
+ import express from 'express';
7
+ import { createToolConfig } from './common/tool-schema.js';
8
+ import { KeywordSearchMcpTool, SemanticSearchMcpTool } from './noc/index.js';
9
+ const allTools = [
10
+ KeywordSearchMcpTool,
11
+ SemanticSearchMcpTool,
12
+ ];
13
+ const SERVER_NAME = 'immicore-noc';
14
+ const SERVER_VERSION = '1.0.0';
15
+ const DEFAULT_HTTP_PORT = 3008;
16
+ function createServer() {
17
+ const server = new McpServer({
18
+ name: SERVER_NAME,
19
+ version: SERVER_VERSION,
20
+ });
21
+ for (const tool of allTools) {
22
+ const config = createToolConfig(tool);
23
+ server.registerTool(tool.name, {
24
+ title: config.title,
25
+ description: config.description,
26
+ inputSchema: config.inputSchema,
27
+ annotations: config.annotations,
28
+ }, async (args) => {
29
+ const result = await tool.call(args);
30
+ return {
31
+ content: result.content,
32
+ structuredContent: result.structuredContent,
33
+ isError: result.isError,
34
+ };
35
+ });
36
+ }
37
+ return server;
38
+ }
39
+ async function runStdioMode() {
40
+ const server = createServer();
41
+ const transport = new StdioServerTransport();
42
+ await server.connect(transport);
43
+ console.error(`${SERVER_NAME} running in stdio mode`);
44
+ }
45
+ async function runHttpMode(port) {
46
+ const app = express();
47
+ app.use(express.json());
48
+ app.post('/mcp', async (req, res) => {
49
+ const server = createServer();
50
+ const transport = new StreamableHTTPServerTransport({
51
+ sessionIdGenerator: undefined,
52
+ enableJsonResponse: true
53
+ });
54
+ res.on('close', () => transport.close());
55
+ await server.connect(transport);
56
+ await transport.handleRequest(req, res, req.body);
57
+ });
58
+ app.get('/health', (_req, res) => {
59
+ res.json({ status: 'ok', server: SERVER_NAME, version: SERVER_VERSION });
60
+ });
61
+ app.listen(port, () => {
62
+ console.log(`${SERVER_NAME} running in HTTP mode on http://localhost:${port}/mcp`);
63
+ });
64
+ }
65
+ async function runSseMode(port) {
66
+ const app = express();
67
+ app.use(express.json());
68
+ const sessions = new Map();
69
+ app.get('/sse', (req, res) => {
70
+ const server = createServer();
71
+ const transport = new SSEServerTransport('/messages', res);
72
+ const sessionId = transport.sessionId;
73
+ sessions.set(sessionId, { server, transport });
74
+ res.on('close', () => {
75
+ sessions.delete(sessionId);
76
+ });
77
+ server.connect(transport);
78
+ });
79
+ app.post('/messages', (req, res) => {
80
+ const sessionId = req.query.sessionId;
81
+ const session = sessions.get(sessionId);
82
+ if (!session) {
83
+ res.status(404).json({ error: 'Session not found' });
84
+ return;
85
+ }
86
+ session.transport.handlePostMessage(req, res, req.body);
87
+ });
88
+ app.get('/health', (_req, res) => {
89
+ res.json({ status: 'ok', server: SERVER_NAME, version: SERVER_VERSION, mode: 'sse' });
90
+ });
91
+ app.listen(port, () => {
92
+ console.log(`${SERVER_NAME} running in SSE mode on http://localhost:${port}/sse`);
93
+ });
94
+ }
95
+ function parseArgs() {
96
+ const args = process.argv.slice(2);
97
+ let mode = 'stdio';
98
+ let port = DEFAULT_HTTP_PORT;
99
+ for (let i = 0; i < args.length; i++) {
100
+ if (args[i] === '--http') {
101
+ mode = 'http';
102
+ }
103
+ else if (args[i] === '--sse') {
104
+ mode = 'sse';
105
+ }
106
+ else if (args[i] === '--stdio') {
107
+ mode = 'stdio';
108
+ }
109
+ else if (args[i] === '--port' && args[i + 1]) {
110
+ port = parseInt(args[i + 1], 10);
111
+ i++;
112
+ }
113
+ }
114
+ return { mode, port };
115
+ }
116
+ async function main() {
117
+ const { mode, port } = parseArgs();
118
+ if (mode === 'http') {
119
+ await runHttpMode(port);
120
+ }
121
+ else if (mode === 'sse') {
122
+ await runSseMode(port);
123
+ }
124
+ else {
125
+ await runStdioMode();
126
+ }
127
+ }
128
+ main().catch(console.error);
@@ -0,0 +1,3 @@
1
+ export { KeywordSearchMcpTool, KeywordSearchInputSchema, KeywordSearchOutputSchema } from './keyword_search.tool.js';
2
+ export { SemanticSearchMcpTool, SemanticSearchInputSchema, SemanticSearchOutputSchema } from './semantic_search.tool.js';
3
+ export { keywordSearchPrompts, semanticSearchPrompts } from './prompts.js';
@@ -0,0 +1,32 @@
1
+ import axios from 'axios';
2
+ import { requireEnv } from '../common/tool-schema.js';
3
+ function getNocRetrieveApiUrl() {
4
+ const HOST_URL = requireEnv('HOST_URL');
5
+ return `${HOST_URL}/noc/keyword-noc-retrieve`;
6
+ }
7
+ function getSystemToken() {
8
+ return requireEnv('SEARCH_SERVICE_TOKEN');
9
+ }
10
+ export async function keywordSearchHandler(input) {
11
+ try {
12
+ const response = await axios.post(getNocRetrieveApiUrl(), input, {
13
+ timeout: 30000,
14
+ headers: { Authorization: `Bearer ${getSystemToken()}` },
15
+ });
16
+ return response.data;
17
+ }
18
+ catch (error) {
19
+ if (axios.isAxiosError(error)) {
20
+ console.error('Error calling keyword-noc-retrieve API:', error.message);
21
+ if (error.response) {
22
+ console.error('Response data:', error.response.data);
23
+ console.error('Response status:', error.response.status);
24
+ }
25
+ throw new Error(`Failed to retrieve NOC entries: ${error.response?.data?.detail || error.message}`);
26
+ }
27
+ else {
28
+ console.error('Unexpected error:', error);
29
+ throw new Error('An unexpected error occurred while retrieving NOC entries.');
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,87 @@
1
+ import { z } from 'zod';
2
+ import { keywordSearchHandler } from './keyword_search.handler.js';
3
+ export const KeywordSearchInputSchema = z.object({
4
+ main_duties: z.string().regex(/^[\x00-\x7F]*$/, { message: 'Main duties must be in English and contain only ASCII characters.' }).optional().describe('Main duties for the NOC entry. This should be a concise description of the primary responsibilities, in English. Example: "manage software development projects"'),
5
+ top_k: z.number().int().optional().default(5).describe('The number of top results to retrieve. Defaults to 5.'),
6
+ title: z.string().regex(/^[\x00-\x7F]*$/, { message: 'Title must be in English and contain only ASCII characters.' }).optional().describe('The title for the NOC entry. This should be a job title, in English. Example: "Software Project Manager"')
7
+ }).describe('Input schema for the keyword_search tool. All text inputs (main_duties, title) must be in English. If both title and main_duties are omitted, the top_k NOC entries from the entire dataset will be returned.');
8
+ export const KeywordSearchOutputSchema = z.array(z.object({
9
+ noc_code: z.string().describe('The NOC code.'),
10
+ title: z.string().describe('The title of the NOC entry.'),
11
+ title_examples: z.array(z.string()).describe('Examples of titles for the NOC entry.'),
12
+ main_duties: z.array(z.string()).describe('Main duties associated with the NOC entry.'),
13
+ employment_requirement: z.string().describe('Employment requirement for the NOC entry.'),
14
+ additional_information: z.array(z.string()).describe('Additional information for the NOC entry.'),
15
+ exclusion: z.array(z.string()).describe('Exclusions for the NOC entry.'),
16
+ })).describe('Output schema for the keyword_search tool.');
17
+ export const NOC_KEYWORD_SEARCH_DESCRIPTION = `Performs a keyword search for National Occupational Classification (NOC) entries using Elasticsearch.
18
+ This tool helps find NOC codes based on job titles and main duties.
19
+ All text inputs (title, main_duties) must be in English.
20
+ If both 'title' and 'main_duties' are omitted, the tool will return the top_k entries from the entire NOC dataset.
21
+
22
+ Input Parameters:
23
+ - title (string, optional): The job title to search for (must be in English). Example: "Software Engineer".
24
+ - main_duties (string, optional): A description of the main duties or responsibilities for the role (must be in English). Example: "design, develop, and test software applications".
25
+ - top_k (number, optional): The number of top results to retrieve. Defaults to 5.
26
+
27
+ Example Usage:
28
+ {
29
+ "title": "Marketing Manager",
30
+ "main_duties": "develop and implement marketing strategies, manage campaigns, analyze market trends",
31
+ "top_k": 3
32
+ }
33
+
34
+ The tool returns a list of matching NOC entries, including the NOC code, title, example titles, main duties, employment requirements, additional information, and exclusions.`;
35
+ export const KeywordSearchMcpTool = {
36
+ name: 'noc_keyword_search',
37
+ description: NOC_KEYWORD_SEARCH_DESCRIPTION,
38
+ inputShape: KeywordSearchInputSchema,
39
+ outputSchema: KeywordSearchOutputSchema,
40
+ annotations: {
41
+ title: 'NOC Keyword Search',
42
+ readOnlyHint: true,
43
+ destructiveHint: false,
44
+ idempotentHint: true,
45
+ openWorldHint: true,
46
+ },
47
+ call: async (rawInput) => {
48
+ try {
49
+ const processedInput = {
50
+ ...rawInput,
51
+ title: rawInput.title ?? "",
52
+ main_duties: rawInput.main_duties ?? ""
53
+ };
54
+ const nocEntriesArray = await keywordSearchHandler(processedInput);
55
+ if (!nocEntriesArray || nocEntriesArray.length === 0) {
56
+ return {
57
+ content: [{ type: 'text', text: 'No NOC entries found for the given criteria.' }],
58
+ structuredContent: { results: [], total: 0 },
59
+ _meta: { total: 0 }
60
+ };
61
+ }
62
+ return {
63
+ content: nocEntriesArray.map(entry => ({
64
+ type: 'text',
65
+ text: [
66
+ entry.noc_code ? `NOC Code: ${entry.noc_code}` : 'NOC Code: Not specified',
67
+ entry.title ? `Title: ${entry.title}` : 'Title: Not specified',
68
+ entry.title_examples?.length ? `Title Examples: \n - ${entry.title_examples.join('\n - ')}` : '',
69
+ entry.main_duties?.length ? `Main Duties: \n - ${entry.main_duties.join('\n - ')}` : '',
70
+ entry.employment_requirement ? `Employment Requirement: ${entry.employment_requirement}` : ''
71
+ ].filter(Boolean).join('\n\n')
72
+ })),
73
+ structuredContent: { results: nocEntriesArray, total: nocEntriesArray.length },
74
+ _meta: { total: nocEntriesArray.length }
75
+ };
76
+ }
77
+ catch (error) {
78
+ const errorMessage = error instanceof Error ? error.message : 'Failed to retrieve NOC entries.';
79
+ console.error('Error in noc_keyword_search tool call:', error);
80
+ return {
81
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
82
+ isError: true,
83
+ _meta: { error: true }
84
+ };
85
+ }
86
+ },
87
+ };
@@ -0,0 +1,27 @@
1
+ export const keywordSearchPrompts = [
2
+ {
3
+ name: 'noc_keyword_search', // Corresponds to NocKeywordSearchMcpTool.name
4
+ description: 'Search NOC information using keywords. User can specify query text, NOC code, and top_k results.',
5
+ arguments: [
6
+ { name: 'query_text', type: 'string', description: 'The search query text for NOC keywords.', required: true },
7
+ { name: 'noc_code', type: 'string', description: 'Optional NOC code to filter by (e.g., "2173").', required: false },
8
+ { name: 'top_k', type: 'number', description: 'Optional number of results to return.', required: false },
9
+ ],
10
+ },
11
+ ];
12
+ export const semanticSearchPrompts = [
13
+ {
14
+ name: 'noc_semantic_search', // Corresponds to NocSemanticSearchMcpTool.name
15
+ description: 'Perform a semantic search for NOC information. User can specify query text, NOC code, title keywords, and top_k results.',
16
+ arguments: [
17
+ { name: 'query_text', type: 'string', description: 'The natural language query for semantic NOC search.', required: true },
18
+ { name: 'noc_code', type: 'string', description: 'Optional NOC code to filter by (e.g., "2173").', required: false },
19
+ { name: 'title', type: 'string', description: 'Optional keywords to filter by NOC title.', required: false },
20
+ { name: 'top_k', type: 'number', description: 'Optional number of results to return.', required: false },
21
+ ],
22
+ },
23
+ ];
24
+ export const nocToolPrompts = {
25
+ keyword_search: keywordSearchPrompts, // Tool name from NocKeywordSearchMcpTool
26
+ semantic_search: semanticSearchPrompts, // Tool name from NocSemanticSearchMcpTool
27
+ };
@@ -0,0 +1,32 @@
1
+ import axios from 'axios';
2
+ import { requireEnv } from '../common/tool-schema.js';
3
+ function getNocSearchApiUrl() {
4
+ const HOST_URL = requireEnv('HOST_URL');
5
+ return `${HOST_URL}/noc/semantic-noc-search`;
6
+ }
7
+ function getSystemToken() {
8
+ return requireEnv('SEARCH_SERVICE_TOKEN');
9
+ }
10
+ export async function semanticSearchHandler(input) {
11
+ try {
12
+ const response = await axios.post(getNocSearchApiUrl(), input, {
13
+ timeout: 30000,
14
+ headers: { Authorization: `Bearer ${getSystemToken()}` },
15
+ });
16
+ return response.data;
17
+ }
18
+ catch (error) {
19
+ if (axios.isAxiosError(error)) {
20
+ console.error('Error calling semantic-noc-search API:', error.message);
21
+ if (error.response) {
22
+ console.error('Response data:', error.response.data);
23
+ console.error('Response status:', error.response.status);
24
+ }
25
+ throw new Error(`Failed to search NOC entries: ${error.response?.data?.detail || error.message}`);
26
+ }
27
+ else {
28
+ console.error('Unexpected error:', error);
29
+ throw new Error('An unexpected error occurred while searching NOC entries.');
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { semanticSearchHandler } from './semantic_search.handler.js';
3
+ export const SemanticSearchInputSchema = z.object({
4
+ query_text: z.string().regex(/^[\x00-\x7F]+$/, { message: 'Query text must be in English.' }).describe('The semantic query text for NOC entries. This should be a natural language description of a job, its duties, or skills, in English. Example: "seeking a professional who can lead marketing initiatives and manage a team"'),
5
+ top_k: z.number().int().optional().default(5).describe('The number of top results to retrieve. Defaults to 5.')
6
+ }).describe('Input schema for the semantic_search tool. The query_text must be in English.');
7
+ export const SemanticSearchOutputSchema = z.object({
8
+ results: z.array(z.object({
9
+ noc_code: z.string().describe('The NOC code.'),
10
+ title: z.string().describe('The title of the NOC entry.'),
11
+ title_examples: z.array(z.string()).describe('Examples of titles for the NOC entry.'),
12
+ main_duties: z.array(z.string()).describe('Main duties associated with the NOC entry.'),
13
+ employment_requirement: z.string().describe('Employment requirement for the NOC entry.'),
14
+ additional_information: z.array(z.string()).describe('Additional information for the NOC entry.'),
15
+ exclusion: z.array(z.string()).describe('Exclusions for the NOC entry.'),
16
+ })).describe('List of NOC entries matching the semantic search.'),
17
+ total: z.number().int().describe('The total number of results found.'),
18
+ }).describe('Output schema for the semantic_search tool.');
19
+ export const NOC_SEMANTIC_SEARCH_DESCRIPTION = `Performs a semantic search for National Occupational Classification (NOC) entries using Supabase.
20
+ This tool is useful for finding NOC codes when you can describe a role or its duties in natural language, rather than specific keywords.
21
+ The input query_text must be in English.
22
+
23
+ Input Parameters:
24
+ - query_text (string, required): A natural language description of the job, its duties, or required skills (must be in English). Example: "Looking for a job that involves creating visual concepts, by hand or using computer software, to communicate ideas that inspire, inform, or captivate consumers."
25
+ - top_k (number, optional): The number of top results to retrieve. Defaults to 5.
26
+
27
+ Example Usage:
28
+ {
29
+ "query_text": "Manages company's financial planning, reporting, and ensures compliance with financial regulations.",
30
+ "top_k": 3
31
+ }
32
+
33
+ The tool returns a list of NOC entries that are semantically similar to the query_text.`;
34
+ export const SemanticSearchMcpTool = {
35
+ name: 'noc_semantic_search',
36
+ description: NOC_SEMANTIC_SEARCH_DESCRIPTION,
37
+ inputShape: SemanticSearchInputSchema,
38
+ outputSchema: SemanticSearchOutputSchema,
39
+ annotations: {
40
+ title: 'NOC Semantic Search',
41
+ readOnlyHint: true,
42
+ destructiveHint: false,
43
+ idempotentHint: true,
44
+ openWorldHint: true,
45
+ },
46
+ call: async (input) => {
47
+ try {
48
+ const result = await semanticSearchHandler(input);
49
+ if (!result.results || result.results.length === 0) {
50
+ return {
51
+ content: [{ type: 'text', text: `No NOC entries found for query "${input.query_text}".` }],
52
+ structuredContent: { results: [], total: 0 },
53
+ _meta: { total: 0 }
54
+ };
55
+ }
56
+ return {
57
+ content: result.results.map(entry => ({
58
+ type: 'text',
59
+ text: [
60
+ entry.noc_code ? `NOC Code: ${entry.noc_code}` : '',
61
+ entry.title ? `Title: ${entry.title}` : '',
62
+ entry.title_examples?.length ? `Title Examples: ${entry.title_examples.slice(0, 3).join(', ')}` : '',
63
+ entry.main_duties?.length ? `Main Duties: ${entry.main_duties.slice(0, 3).join('; ')}` : ''
64
+ ].filter(Boolean).join('\n')
65
+ })),
66
+ structuredContent: result,
67
+ _meta: { total: result.total }
68
+ };
69
+ }
70
+ catch (error) {
71
+ const errorMessage = error instanceof Error ? error.message : 'Failed to search NOC entries.';
72
+ console.error('Error in noc_semantic_search tool call:', error);
73
+ return {
74
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
75
+ isError: true,
76
+ _meta: { error: true }
77
+ };
78
+ }
79
+ },
80
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@immicore/noc",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for National Occupational Classification (NOC) search",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "immicore-noc": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx src/index.ts --stdio",
12
+ "dev:http": "tsx src/index.ts --http --port 3008",
13
+ "dev:sse": "tsx src/index.ts --sse --port 3008",
14
+ "build": "tsc",
15
+ "start": "node dist/index.js --stdio",
16
+ "start:http": "node dist/index.js --http --port 3008",
17
+ "start:sse": "node dist/index.js --sse --port 3008"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.25.1",
21
+ "axios": "^1.8.4",
22
+ "dotenv": "^17.2.3",
23
+ "express": "^4.21.2",
24
+ "zod": "^3.22.4"
25
+ },
26
+ "devDependencies": {
27
+ "@types/express": "^5.0.0",
28
+ "@types/node": "^22.15.2",
29
+ "tsx": "^4.16.2",
30
+ "typescript": "^5.4.5"
31
+ }
32
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Zod schema for a single prompt template, with arguments.
5
+ */
6
+ export const promptSchema = z.object({
7
+ name: z.string().describe('Prompt unique name.'),
8
+ description: z.string().optional().describe('Prompt description.'),
9
+ arguments: z.array(
10
+ z.object({
11
+ name: z.string().describe('Argument name.'),
12
+ description: z.string().optional().describe('Argument description.'),
13
+ required: z.boolean().optional().describe('Is this argument required?'),
14
+ type: z.string().describe('Argument type (e.g., string, number, boolean, enum, etc.)'),
15
+ })
16
+ ).optional().describe('Prompt arguments.'),
17
+ });
18
+
19
+ export type Prompt = z.infer<typeof promptSchema>;
20
+ export type PromptArgument = Prompt['arguments'] extends Array<infer T> ? T : never;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Zod schema for a single resource.
5
+ */
6
+ export const resourceSchema = z.object({
7
+ id: z.string(),
8
+ type: z.string().optional(),
9
+ label: z.string().optional(),
10
+ name: z.string().optional(),
11
+ description: z.string().optional(),
12
+ uri: z.string().optional(), // 允许 formbro://xxx 等自定义 scheme
13
+ mimeType: z.string().optional(),
14
+ meta: z.record(z.any()).optional(),
15
+ });
16
+
17
+ export type Resource = z.infer<typeof resourceSchema>;
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Tool annotations for MCP registerTool API
5
+ */
6
+ export interface ToolAnnotations {
7
+ title?: string;
8
+ readOnlyHint?: boolean;
9
+ destructiveHint?: boolean;
10
+ idempotentHint?: boolean;
11
+ openWorldHint?: boolean;
12
+ }
13
+
14
+ /**
15
+ * MCP Tool response content item - matches SDK types
16
+ */
17
+ export type ToolContentItem = {
18
+ type: 'text';
19
+ text: string;
20
+ };
21
+
22
+ /**
23
+ * MCP Tool response structure - matches SDK CallToolResult
24
+ */
25
+ export interface ToolResponse<T = unknown> {
26
+ content: ToolContentItem[];
27
+ structuredContent?: T;
28
+ isError?: boolean;
29
+ _meta?: Record<string, unknown>;
30
+ }
31
+
32
+ /**
33
+ * Tool definition interface for MCP servers
34
+ */
35
+ export interface Tool<
36
+ TInput extends z.ZodTypeAny = z.ZodTypeAny,
37
+ TOutput = unknown
38
+ > {
39
+ name: string;
40
+ description: string;
41
+ inputShape: TInput;
42
+ outputSchema?: z.ZodTypeAny;
43
+ annotations?: ToolAnnotations;
44
+ call: (args: z.infer<TInput>) => Promise<ToolResponse<TOutput>>;
45
+ }
46
+
47
+ /**
48
+ * Configuration for registering tools with McpServer
49
+ */
50
+ export interface ToolRegistrationConfig {
51
+ title: string;
52
+ description: string;
53
+ inputSchema: z.ZodTypeAny;
54
+ outputSchema?: z.ZodTypeAny;
55
+ annotations: ToolAnnotations;
56
+ }
57
+
58
+ /**
59
+ * Helper to create tool registration config from Tool interface
60
+ */
61
+ export function createToolConfig(tool: Tool): ToolRegistrationConfig {
62
+ return {
63
+ title: tool.annotations?.title || tool.name,
64
+ description: tool.description,
65
+ inputSchema: tool.inputShape,
66
+ outputSchema: tool.outputSchema,
67
+ annotations: {
68
+ readOnlyHint: tool.annotations?.readOnlyHint ?? true,
69
+ destructiveHint: tool.annotations?.destructiveHint ?? false,
70
+ idempotentHint: tool.annotations?.idempotentHint ?? true,
71
+ openWorldHint: tool.annotations?.openWorldHint ?? true,
72
+ },
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Get required environment variable or throw
78
+ */
79
+ export function requireEnv(name: string): string {
80
+ const value = process.env[name];
81
+ if (!value) {
82
+ throw new Error(`Required environment variable ${name} is not set`);
83
+ }
84
+ return value;
85
+ }
86
+
87
+ /**
88
+ * Get optional environment variable with default
89
+ */
90
+ export function getEnv(name: string, defaultValue: string): string {
91
+ return process.env[name] || defaultValue;
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
6
+ import express, { Request, Response } from 'express';
7
+
8
+ import { Tool, createToolConfig } from './common/tool-schema.js';
9
+
10
+ import {
11
+ KeywordSearchMcpTool,
12
+ SemanticSearchMcpTool
13
+ } from './noc/index.js';
14
+
15
+ const allTools: Tool[] = [
16
+ KeywordSearchMcpTool,
17
+ SemanticSearchMcpTool,
18
+ ];
19
+
20
+ const SERVER_NAME = 'immicore-noc';
21
+ const SERVER_VERSION = '1.0.0';
22
+ const DEFAULT_HTTP_PORT = 3008;
23
+
24
+ function createServer(): McpServer {
25
+ const server = new McpServer({
26
+ name: SERVER_NAME,
27
+ version: SERVER_VERSION,
28
+ });
29
+
30
+ for (const tool of allTools) {
31
+ const config = createToolConfig(tool);
32
+ server.registerTool(
33
+ tool.name,
34
+ {
35
+ title: config.title,
36
+ description: config.description,
37
+ inputSchema: config.inputSchema,
38
+ annotations: config.annotations,
39
+ },
40
+ async (args: Record<string, unknown>) => {
41
+ const result = await tool.call(args);
42
+ return {
43
+ content: result.content as Array<{ type: 'text'; text: string }>,
44
+ structuredContent: result.structuredContent as Record<string, unknown> | undefined,
45
+ isError: result.isError,
46
+ };
47
+ }
48
+ );
49
+ }
50
+
51
+ return server;
52
+ }
53
+
54
+ async function runStdioMode(): Promise<void> {
55
+ const server = createServer();
56
+ const transport = new StdioServerTransport();
57
+ await server.connect(transport);
58
+ console.error(`${SERVER_NAME} running in stdio mode`);
59
+ }
60
+
61
+ async function runHttpMode(port: number): Promise<void> {
62
+ const app = express();
63
+ app.use(express.json());
64
+
65
+ app.post('/mcp', async (req, res) => {
66
+ const server = createServer();
67
+ const transport = new StreamableHTTPServerTransport({
68
+ sessionIdGenerator: undefined,
69
+ enableJsonResponse: true
70
+ });
71
+
72
+ res.on('close', () => transport.close());
73
+
74
+ await server.connect(transport);
75
+ await transport.handleRequest(req, res, req.body);
76
+ });
77
+
78
+ app.get('/health', (_req, res) => {
79
+ res.json({ status: 'ok', server: SERVER_NAME, version: SERVER_VERSION });
80
+ });
81
+
82
+ app.listen(port, () => {
83
+ console.log(`${SERVER_NAME} running in HTTP mode on http://localhost:${port}/mcp`);
84
+ });
85
+ }
86
+
87
+ async function runSseMode(port: number): Promise<void> {
88
+ const app = express();
89
+ app.use(express.json());
90
+
91
+ const sessions: Map<string, { server: McpServer; transport: SSEServerTransport }> = new Map();
92
+
93
+ app.get('/sse', (req: Request, res: Response) => {
94
+ const server = createServer();
95
+ const transport = new SSEServerTransport('/messages', res);
96
+ const sessionId = transport.sessionId;
97
+
98
+ sessions.set(sessionId, { server, transport });
99
+
100
+ res.on('close', () => {
101
+ sessions.delete(sessionId);
102
+ });
103
+
104
+ server.connect(transport);
105
+ });
106
+
107
+ app.post('/messages', (req: Request, res: Response) => {
108
+ const sessionId = req.query.sessionId as string;
109
+ const session = sessions.get(sessionId);
110
+
111
+ if (!session) {
112
+ res.status(404).json({ error: 'Session not found' });
113
+ return;
114
+ }
115
+
116
+ session.transport.handlePostMessage(req, res, req.body);
117
+ });
118
+
119
+ app.get('/health', (_req, res) => {
120
+ res.json({ status: 'ok', server: SERVER_NAME, version: SERVER_VERSION, mode: 'sse' });
121
+ });
122
+
123
+ app.listen(port, () => {
124
+ console.log(`${SERVER_NAME} running in SSE mode on http://localhost:${port}/sse`);
125
+ });
126
+ }
127
+
128
+ function parseArgs(): { mode: 'stdio' | 'http' | 'sse'; port: number } {
129
+ const args = process.argv.slice(2);
130
+ let mode: 'stdio' | 'http' | 'sse' = 'stdio';
131
+ let port = DEFAULT_HTTP_PORT;
132
+
133
+ for (let i = 0; i < args.length; i++) {
134
+ if (args[i] === '--http') {
135
+ mode = 'http';
136
+ } else if (args[i] === '--sse') {
137
+ mode = 'sse';
138
+ } else if (args[i] === '--stdio') {
139
+ mode = 'stdio';
140
+ } else if (args[i] === '--port' && args[i + 1]) {
141
+ port = parseInt(args[i + 1], 10);
142
+ i++;
143
+ }
144
+ }
145
+
146
+ return { mode, port };
147
+ }
148
+
149
+ async function main(): Promise<void> {
150
+ const { mode, port } = parseArgs();
151
+
152
+ if (mode === 'http') {
153
+ await runHttpMode(port);
154
+ } else if (mode === 'sse') {
155
+ await runSseMode(port);
156
+ } else {
157
+ await runStdioMode();
158
+ }
159
+ }
160
+
161
+ main().catch(console.error);
@@ -0,0 +1,13 @@
1
+ export {
2
+ KeywordSearchMcpTool,
3
+ KeywordSearchInputSchema,
4
+ KeywordSearchOutputSchema
5
+ } from './keyword_search.tool.js';
6
+
7
+ export {
8
+ SemanticSearchMcpTool,
9
+ SemanticSearchInputSchema,
10
+ SemanticSearchOutputSchema
11
+ } from './semantic_search.tool.js';
12
+
13
+ export { keywordSearchPrompts, semanticSearchPrompts } from './prompts.js';
@@ -0,0 +1,37 @@
1
+ import axios from 'axios';
2
+ import { z } from 'zod';
3
+ import { KeywordSearchInputSchema, KeywordSearchOutputSchema } from './keyword_search.tool.js';
4
+ import { requireEnv } from '../common/tool-schema.js';
5
+
6
+ function getNocRetrieveApiUrl(): string {
7
+ const HOST_URL = requireEnv('HOST_URL');
8
+ return `${HOST_URL}/noc/keyword-noc-retrieve`;
9
+ }
10
+
11
+ function getSystemToken(): string {
12
+ return requireEnv('SEARCH_SERVICE_TOKEN');
13
+ }
14
+
15
+ export async function keywordSearchHandler(
16
+ input: z.infer<typeof KeywordSearchInputSchema>
17
+ ): Promise<z.infer<typeof KeywordSearchOutputSchema>> {
18
+ try {
19
+ const response = await axios.post(getNocRetrieveApiUrl(), input, {
20
+ timeout: 30000,
21
+ headers: { Authorization: `Bearer ${getSystemToken()}` },
22
+ });
23
+ return response.data;
24
+ } catch (error) {
25
+ if (axios.isAxiosError(error)) {
26
+ console.error('Error calling keyword-noc-retrieve API:', error.message);
27
+ if (error.response) {
28
+ console.error('Response data:', error.response.data);
29
+ console.error('Response status:', error.response.status);
30
+ }
31
+ throw new Error(`Failed to retrieve NOC entries: ${error.response?.data?.detail || error.message}`);
32
+ } else {
33
+ console.error('Unexpected error:', error);
34
+ throw new Error('An unexpected error occurred while retrieving NOC entries.');
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod';
2
+ import { Tool, ToolResponse } from '../common/tool-schema.js';
3
+ import { keywordSearchHandler } from './keyword_search.handler.js';
4
+
5
+ export const KeywordSearchInputSchema = z.object({
6
+ main_duties: z.string().regex(/^[\x00-\x7F]*$/, { message: 'Main duties must be in English and contain only ASCII characters.' }).optional().describe('Main duties for the NOC entry. This should be a concise description of the primary responsibilities, in English. Example: "manage software development projects"'),
7
+ top_k: z.number().int().optional().default(5).describe('The number of top results to retrieve. Defaults to 5.'),
8
+ title: z.string().regex(/^[\x00-\x7F]*$/, { message: 'Title must be in English and contain only ASCII characters.' }).optional().describe('The title for the NOC entry. This should be a job title, in English. Example: "Software Project Manager"')
9
+ }).describe('Input schema for the keyword_search tool. All text inputs (main_duties, title) must be in English. If both title and main_duties are omitted, the top_k NOC entries from the entire dataset will be returned.');
10
+
11
+ export const KeywordSearchOutputSchema = z.array(z.object({
12
+ noc_code: z.string().describe('The NOC code.'),
13
+ title: z.string().describe('The title of the NOC entry.'),
14
+ title_examples: z.array(z.string()).describe('Examples of titles for the NOC entry.'),
15
+ main_duties: z.array(z.string()).describe('Main duties associated with the NOC entry.'),
16
+ employment_requirement: z.string().describe('Employment requirement for the NOC entry.'),
17
+ additional_information: z.array(z.string()).describe('Additional information for the NOC entry.'),
18
+ exclusion: z.array(z.string()).describe('Exclusions for the NOC entry.'),
19
+ })).describe('Output schema for the keyword_search tool.');
20
+
21
+ export const NOC_KEYWORD_SEARCH_DESCRIPTION = `Performs a keyword search for National Occupational Classification (NOC) entries using Elasticsearch.
22
+ This tool helps find NOC codes based on job titles and main duties.
23
+ All text inputs (title, main_duties) must be in English.
24
+ If both 'title' and 'main_duties' are omitted, the tool will return the top_k entries from the entire NOC dataset.
25
+
26
+ Input Parameters:
27
+ - title (string, optional): The job title to search for (must be in English). Example: "Software Engineer".
28
+ - main_duties (string, optional): A description of the main duties or responsibilities for the role (must be in English). Example: "design, develop, and test software applications".
29
+ - top_k (number, optional): The number of top results to retrieve. Defaults to 5.
30
+
31
+ Example Usage:
32
+ {
33
+ "title": "Marketing Manager",
34
+ "main_duties": "develop and implement marketing strategies, manage campaigns, analyze market trends",
35
+ "top_k": 3
36
+ }
37
+
38
+ The tool returns a list of matching NOC entries, including the NOC code, title, example titles, main duties, employment requirements, additional information, and exclusions.`;
39
+
40
+ type KeywordSearchOutput = z.infer<typeof KeywordSearchOutputSchema>;
41
+
42
+ export const KeywordSearchMcpTool: Tool = {
43
+ name: 'noc_keyword_search',
44
+ description: NOC_KEYWORD_SEARCH_DESCRIPTION,
45
+ inputShape: KeywordSearchInputSchema,
46
+ outputSchema: KeywordSearchOutputSchema,
47
+ annotations: {
48
+ title: 'NOC Keyword Search',
49
+ readOnlyHint: true,
50
+ destructiveHint: false,
51
+ idempotentHint: true,
52
+ openWorldHint: true,
53
+ },
54
+ call: async (rawInput: z.infer<typeof KeywordSearchInputSchema>): Promise<ToolResponse> => {
55
+ try {
56
+ const processedInput = {
57
+ ...rawInput,
58
+ title: rawInput.title ?? "",
59
+ main_duties: rawInput.main_duties ?? ""
60
+ };
61
+
62
+ const nocEntriesArray = await keywordSearchHandler(processedInput);
63
+
64
+ if (!nocEntriesArray || nocEntriesArray.length === 0) {
65
+ return {
66
+ content: [{ type: 'text' as const, text: 'No NOC entries found for the given criteria.' }],
67
+ structuredContent: { results: [], total: 0 },
68
+ _meta: { total: 0 }
69
+ };
70
+ }
71
+
72
+ return {
73
+ content: nocEntriesArray.map(entry => ({
74
+ type: 'text' as const,
75
+ text: [
76
+ entry.noc_code ? `NOC Code: ${entry.noc_code}` : 'NOC Code: Not specified',
77
+ entry.title ? `Title: ${entry.title}` : 'Title: Not specified',
78
+ entry.title_examples?.length ? `Title Examples: \n - ${entry.title_examples.join('\n - ')}` : '',
79
+ entry.main_duties?.length ? `Main Duties: \n - ${entry.main_duties.join('\n - ')}` : '',
80
+ entry.employment_requirement ? `Employment Requirement: ${entry.employment_requirement}` : ''
81
+ ].filter(Boolean).join('\n\n')
82
+ })),
83
+ structuredContent: { results: nocEntriesArray, total: nocEntriesArray.length },
84
+ _meta: { total: nocEntriesArray.length }
85
+ };
86
+ } catch (error: unknown) {
87
+ const errorMessage = error instanceof Error ? error.message : 'Failed to retrieve NOC entries.';
88
+ console.error('Error in noc_keyword_search tool call:', error);
89
+ return {
90
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
91
+ isError: true,
92
+ _meta: { error: true }
93
+ };
94
+ }
95
+ },
96
+ };
@@ -0,0 +1,31 @@
1
+ import { Prompt } from '../common/prompt-schema.js';
2
+
3
+ export const keywordSearchPrompts: Prompt[] = [
4
+ {
5
+ name: 'noc_keyword_search', // Corresponds to NocKeywordSearchMcpTool.name
6
+ description: 'Search NOC information using keywords. User can specify query text, NOC code, and top_k results.',
7
+ arguments: [
8
+ { name: 'query_text', type: 'string', description: 'The search query text for NOC keywords.', required: true },
9
+ { name: 'noc_code', type: 'string', description: 'Optional NOC code to filter by (e.g., "2173").', required: false },
10
+ { name: 'top_k', type: 'number', description: 'Optional number of results to return.', required: false },
11
+ ],
12
+ },
13
+ ];
14
+
15
+ export const semanticSearchPrompts: Prompt[] = [
16
+ {
17
+ name: 'noc_semantic_search', // Corresponds to NocSemanticSearchMcpTool.name
18
+ description: 'Perform a semantic search for NOC information. User can specify query text, NOC code, title keywords, and top_k results.',
19
+ arguments: [
20
+ { name: 'query_text', type: 'string', description: 'The natural language query for semantic NOC search.', required: true },
21
+ { name: 'noc_code', type: 'string', description: 'Optional NOC code to filter by (e.g., "2173").', required: false },
22
+ { name: 'title', type: 'string', description: 'Optional keywords to filter by NOC title.', required: false },
23
+ { name: 'top_k', type: 'number', description: 'Optional number of results to return.', required: false },
24
+ ],
25
+ },
26
+ ];
27
+
28
+ export const nocToolPrompts: Record<string, Prompt[]> = {
29
+ keyword_search: keywordSearchPrompts, // Tool name from NocKeywordSearchMcpTool
30
+ semantic_search: semanticSearchPrompts, // Tool name from NocSemanticSearchMcpTool
31
+ };
@@ -0,0 +1,37 @@
1
+ import axios from 'axios';
2
+ import { z } from 'zod';
3
+ import { SemanticSearchInputSchema, SemanticSearchOutputSchema } from './semantic_search.tool.js';
4
+ import { requireEnv } from '../common/tool-schema.js';
5
+
6
+ function getNocSearchApiUrl(): string {
7
+ const HOST_URL = requireEnv('HOST_URL');
8
+ return `${HOST_URL}/noc/semantic-noc-search`;
9
+ }
10
+
11
+ function getSystemToken(): string {
12
+ return requireEnv('SEARCH_SERVICE_TOKEN');
13
+ }
14
+
15
+ export async function semanticSearchHandler(
16
+ input: z.infer<typeof SemanticSearchInputSchema>
17
+ ): Promise<z.infer<typeof SemanticSearchOutputSchema>> {
18
+ try {
19
+ const response = await axios.post(getNocSearchApiUrl(), input, {
20
+ timeout: 30000,
21
+ headers: { Authorization: `Bearer ${getSystemToken()}` },
22
+ });
23
+ return response.data;
24
+ } catch (error) {
25
+ if (axios.isAxiosError(error)) {
26
+ console.error('Error calling semantic-noc-search API:', error.message);
27
+ if (error.response) {
28
+ console.error('Response data:', error.response.data);
29
+ console.error('Response status:', error.response.status);
30
+ }
31
+ throw new Error(`Failed to search NOC entries: ${error.response?.data?.detail || error.message}`);
32
+ } else {
33
+ console.error('Unexpected error:', error);
34
+ throw new Error('An unexpected error occurred while searching NOC entries.');
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod';
2
+ import { Tool, ToolResponse } from '../common/tool-schema.js';
3
+ import { semanticSearchHandler } from './semantic_search.handler.js';
4
+
5
+ export const SemanticSearchInputSchema = z.object({
6
+ query_text: z.string().regex(/^[\x00-\x7F]+$/, { message: 'Query text must be in English.' }).describe('The semantic query text for NOC entries. This should be a natural language description of a job, its duties, or skills, in English. Example: "seeking a professional who can lead marketing initiatives and manage a team"'),
7
+ top_k: z.number().int().optional().default(5).describe('The number of top results to retrieve. Defaults to 5.')
8
+ }).describe('Input schema for the semantic_search tool. The query_text must be in English.');
9
+
10
+ export const SemanticSearchOutputSchema = z.object({
11
+ results: z.array(z.object({
12
+ noc_code: z.string().describe('The NOC code.'),
13
+ title: z.string().describe('The title of the NOC entry.'),
14
+ title_examples: z.array(z.string()).describe('Examples of titles for the NOC entry.'),
15
+ main_duties: z.array(z.string()).describe('Main duties associated with the NOC entry.'),
16
+ employment_requirement: z.string().describe('Employment requirement for the NOC entry.'),
17
+ additional_information: z.array(z.string()).describe('Additional information for the NOC entry.'),
18
+ exclusion: z.array(z.string()).describe('Exclusions for the NOC entry.'),
19
+ })).describe('List of NOC entries matching the semantic search.'),
20
+ total: z.number().int().describe('The total number of results found.'),
21
+ }).describe('Output schema for the semantic_search tool.');
22
+
23
+ export const NOC_SEMANTIC_SEARCH_DESCRIPTION = `Performs a semantic search for National Occupational Classification (NOC) entries using Supabase.
24
+ This tool is useful for finding NOC codes when you can describe a role or its duties in natural language, rather than specific keywords.
25
+ The input query_text must be in English.
26
+
27
+ Input Parameters:
28
+ - query_text (string, required): A natural language description of the job, its duties, or required skills (must be in English). Example: "Looking for a job that involves creating visual concepts, by hand or using computer software, to communicate ideas that inspire, inform, or captivate consumers."
29
+ - top_k (number, optional): The number of top results to retrieve. Defaults to 5.
30
+
31
+ Example Usage:
32
+ {
33
+ "query_text": "Manages company's financial planning, reporting, and ensures compliance with financial regulations.",
34
+ "top_k": 3
35
+ }
36
+
37
+ The tool returns a list of NOC entries that are semantically similar to the query_text.`;
38
+
39
+ type SemanticSearchOutput = z.infer<typeof SemanticSearchOutputSchema>;
40
+
41
+ export const SemanticSearchMcpTool: Tool = {
42
+ name: 'noc_semantic_search',
43
+ description: NOC_SEMANTIC_SEARCH_DESCRIPTION,
44
+ inputShape: SemanticSearchInputSchema,
45
+ outputSchema: SemanticSearchOutputSchema,
46
+ annotations: {
47
+ title: 'NOC Semantic Search',
48
+ readOnlyHint: true,
49
+ destructiveHint: false,
50
+ idempotentHint: true,
51
+ openWorldHint: true,
52
+ },
53
+ call: async (input: z.infer<typeof SemanticSearchInputSchema>): Promise<ToolResponse> => {
54
+ try {
55
+ const result = await semanticSearchHandler(input);
56
+
57
+ if (!result.results || result.results.length === 0) {
58
+ return {
59
+ content: [{ type: 'text' as const, text: `No NOC entries found for query "${input.query_text}".` }],
60
+ structuredContent: { results: [], total: 0 },
61
+ _meta: { total: 0 }
62
+ };
63
+ }
64
+
65
+ return {
66
+ content: result.results.map(entry => ({
67
+ type: 'text' as const,
68
+ text: [
69
+ entry.noc_code ? `NOC Code: ${entry.noc_code}` : '',
70
+ entry.title ? `Title: ${entry.title}` : '',
71
+ entry.title_examples?.length ? `Title Examples: ${entry.title_examples.slice(0, 3).join(', ')}` : '',
72
+ entry.main_duties?.length ? `Main Duties: ${entry.main_duties.slice(0, 3).join('; ')}` : ''
73
+ ].filter(Boolean).join('\n')
74
+ })),
75
+ structuredContent: result,
76
+ _meta: { total: result.total }
77
+ };
78
+ } catch (error: unknown) {
79
+ const errorMessage = error instanceof Error ? error.message : 'Failed to search NOC entries.';
80
+ console.error('Error in noc_semantic_search tool call:', error);
81
+ return {
82
+ content: [{ type: 'text' as const, text: `Error: ${errorMessage}` }],
83
+ isError: true,
84
+ _meta: { error: true }
85
+ };
86
+ }
87
+ },
88
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "strictNullChecks": true,
11
+ "skipLibCheck": true,
12
+ "outDir": "dist",
13
+ "rootDir": "src",
14
+ "resolveJsonModule": true,
15
+ "types": ["node"]
16
+ },
17
+ "include": ["src/**/*.ts"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }