@immicore/immi-tools 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/dist/common/tool-schema.d.ts +60 -0
- package/dist/common/tool-schema.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +128 -0
- package/dist/tools/clb_convert.handler.d.ts +3 -0
- package/dist/tools/clb_convert.handler.js +32 -0
- package/dist/tools/clb_convert.tool.d.ts +45 -0
- package/dist/tools/clb_convert.tool.js +93 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/noc_wages.handler.d.ts +4 -0
- package/dist/tools/noc_wages.handler.js +62 -0
- package/dist/tools/noc_wages.tool.d.ts +52 -0
- package/dist/tools/noc_wages.tool.js +96 -0
- package/package.json +39 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tool annotations for MCP registerTool API
|
|
4
|
+
*/
|
|
5
|
+
export interface ToolAnnotations {
|
|
6
|
+
title?: string;
|
|
7
|
+
readOnlyHint?: boolean;
|
|
8
|
+
destructiveHint?: boolean;
|
|
9
|
+
idempotentHint?: boolean;
|
|
10
|
+
openWorldHint?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* MCP Tool response content item - matches SDK types
|
|
14
|
+
*/
|
|
15
|
+
export type ToolContentItem = {
|
|
16
|
+
type: 'text';
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* MCP Tool response structure - matches SDK CallToolResult
|
|
21
|
+
*/
|
|
22
|
+
export interface ToolResponse<T = unknown> {
|
|
23
|
+
content: ToolContentItem[];
|
|
24
|
+
structuredContent?: T;
|
|
25
|
+
isError?: boolean;
|
|
26
|
+
_meta?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Tool definition interface for MCP servers
|
|
30
|
+
*/
|
|
31
|
+
export interface Tool<TInput extends z.ZodTypeAny = z.ZodTypeAny, TOutput = unknown> {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
inputShape: TInput;
|
|
35
|
+
outputSchema?: z.ZodTypeAny;
|
|
36
|
+
annotations?: ToolAnnotations;
|
|
37
|
+
call: (args: z.infer<TInput>) => Promise<ToolResponse<TOutput>>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Configuration for registering tools with McpServer
|
|
41
|
+
*/
|
|
42
|
+
export interface ToolRegistrationConfig {
|
|
43
|
+
title: string;
|
|
44
|
+
description: string;
|
|
45
|
+
inputSchema: z.ZodTypeAny;
|
|
46
|
+
outputSchema?: z.ZodTypeAny;
|
|
47
|
+
annotations: ToolAnnotations;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Helper to create tool registration config from Tool interface
|
|
51
|
+
*/
|
|
52
|
+
export declare function createToolConfig(tool: Tool): ToolRegistrationConfig;
|
|
53
|
+
/**
|
|
54
|
+
* Get required environment variable or throw
|
|
55
|
+
*/
|
|
56
|
+
export declare function requireEnv(name: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Get optional environment variable with default
|
|
59
|
+
*/
|
|
60
|
+
export declare function getEnv(name: string, defaultValue: string): string;
|
|
@@ -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.d.ts
ADDED
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 { CLBConvertMcpTool, NocWagesMcpTool } from './tools/index.js';
|
|
9
|
+
const allTools = [
|
|
10
|
+
CLBConvertMcpTool,
|
|
11
|
+
NocWagesMcpTool,
|
|
12
|
+
];
|
|
13
|
+
const SERVER_NAME = 'immicore-tools';
|
|
14
|
+
const SERVER_VERSION = '1.0.0';
|
|
15
|
+
const DEFAULT_HTTP_PORT = 3009;
|
|
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,32 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { requireEnv } from '../common/tool-schema.js';
|
|
3
|
+
function getClbConvertApiUrl() {
|
|
4
|
+
const HOST_URL = requireEnv('HOST_URL');
|
|
5
|
+
return `${HOST_URL}/tools/clb/convert`;
|
|
6
|
+
}
|
|
7
|
+
function getSystemToken() {
|
|
8
|
+
return requireEnv('SEARCH_SERVICE_TOKEN');
|
|
9
|
+
}
|
|
10
|
+
export async function clbConvertHandler(input) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await axios.post(getClbConvertApiUrl(), input, {
|
|
13
|
+
timeout: 10000,
|
|
14
|
+
headers: { Authorization: `Bearer ${getSystemToken()}` },
|
|
15
|
+
});
|
|
16
|
+
return response.data;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (axios.isAxiosError(error)) {
|
|
20
|
+
console.error('Error calling CLB convert 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 convert to CLB: ${error.response?.data?.detail || error.message}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.error('Unexpected error:', error);
|
|
29
|
+
throw new Error('An unexpected error occurred while converting to CLB.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Tool } from '../common/tool-schema.js';
|
|
3
|
+
export declare const CLBConvertInputSchema: z.ZodObject<{
|
|
4
|
+
test_type: z.ZodEnum<["ielts", "celpip", "tef", "tcf"]>;
|
|
5
|
+
listening: z.ZodNumber;
|
|
6
|
+
reading: z.ZodNumber;
|
|
7
|
+
writing: z.ZodNumber;
|
|
8
|
+
speaking: z.ZodNumber;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
test_type: "ielts" | "celpip" | "tef" | "tcf";
|
|
11
|
+
listening: number;
|
|
12
|
+
reading: number;
|
|
13
|
+
writing: number;
|
|
14
|
+
speaking: number;
|
|
15
|
+
}, {
|
|
16
|
+
test_type: "ielts" | "celpip" | "tef" | "tcf";
|
|
17
|
+
listening: number;
|
|
18
|
+
reading: number;
|
|
19
|
+
writing: number;
|
|
20
|
+
speaking: number;
|
|
21
|
+
}>;
|
|
22
|
+
export declare const CLBConvertOutputSchema: z.ZodObject<{
|
|
23
|
+
test_type: z.ZodString;
|
|
24
|
+
listening: z.ZodNumber;
|
|
25
|
+
reading: z.ZodNumber;
|
|
26
|
+
writing: z.ZodNumber;
|
|
27
|
+
speaking: z.ZodNumber;
|
|
28
|
+
overall: z.ZodNumber;
|
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
test_type: string;
|
|
31
|
+
listening: number;
|
|
32
|
+
reading: number;
|
|
33
|
+
writing: number;
|
|
34
|
+
speaking: number;
|
|
35
|
+
overall: number;
|
|
36
|
+
}, {
|
|
37
|
+
test_type: string;
|
|
38
|
+
listening: number;
|
|
39
|
+
reading: number;
|
|
40
|
+
writing: number;
|
|
41
|
+
speaking: number;
|
|
42
|
+
overall: number;
|
|
43
|
+
}>;
|
|
44
|
+
export declare const CLB_CONVERT_DESCRIPTION = "Convert language test scores to Canadian Language Benchmark (CLB) levels.\n\nSupported test types:\n- IELTS General Training (band scores 0-9)\n- CELPIP General (scores 1-12) \n- TEF Canada (scores 0-600 for all skills)\n- TCF Canada (scores 0-600 for reading/listening, 0-16 for writing/speaking)\n\nReturns CLB levels (0-12) for each skill:\n- listening, reading, writing, speaking\n- overall: minimum of all four skills\n\nExample usage:\n{\n \"test_type\": \"ielts\",\n \"listening\": 7.5,\n \"reading\": 7.0,\n \"writing\": 6.5,\n \"speaking\": 7.0\n}\n\nReturns:\n{\n \"test_type\": \"ielts\",\n \"listening\": 9,\n \"reading\": 8,\n \"writing\": 7,\n \"speaking\": 8,\n \"overall\": 7\n}\n\nUse this tool when:\n- Converting client's language test scores to CLB for immigration eligibility\n- Checking Express Entry CRS score requirements\n- Verifying IRCC language requirements for various programs";
|
|
45
|
+
export declare const CLBConvertMcpTool: Tool;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { clbConvertHandler } from './clb_convert.handler.js';
|
|
3
|
+
export const CLBConvertInputSchema = z.object({
|
|
4
|
+
test_type: z.enum(['ielts', 'celpip', 'tef', 'tcf']).describe('Language test type. Supported: ielts, celpip, tef (TEF Canada), tcf (TCF Canada)'),
|
|
5
|
+
listening: z.number().describe('Listening score. IELTS: 0-9 (band), CELPIP: 1-12, TEF/TCF: 0-600'),
|
|
6
|
+
reading: z.number().describe('Reading score. IELTS: 0-9 (band), CELPIP: 1-12, TEF/TCF: 0-600'),
|
|
7
|
+
writing: z.number().describe('Writing score. IELTS: 0-9 (band), CELPIP: 1-12, TEF: 0-600, TCF: 0-16'),
|
|
8
|
+
speaking: z.number().describe('Speaking score. IELTS: 0-9 (band), CELPIP: 1-12, TEF: 0-600, TCF: 0-16'),
|
|
9
|
+
}).describe('Input for converting language test scores to CLB levels.');
|
|
10
|
+
export const CLBConvertOutputSchema = z.object({
|
|
11
|
+
test_type: z.string().describe('The input test type'),
|
|
12
|
+
listening: z.number().describe('CLB level for listening (0-12)'),
|
|
13
|
+
reading: z.number().describe('CLB level for reading (0-12)'),
|
|
14
|
+
writing: z.number().describe('CLB level for writing (0-12)'),
|
|
15
|
+
speaking: z.number().describe('CLB level for speaking (0-12)'),
|
|
16
|
+
overall: z.number().describe('Overall CLB level (minimum of all skills)'),
|
|
17
|
+
}).describe('CLB conversion result with levels for each skill.');
|
|
18
|
+
export const CLB_CONVERT_DESCRIPTION = `Convert language test scores to Canadian Language Benchmark (CLB) levels.
|
|
19
|
+
|
|
20
|
+
Supported test types:
|
|
21
|
+
- IELTS General Training (band scores 0-9)
|
|
22
|
+
- CELPIP General (scores 1-12)
|
|
23
|
+
- TEF Canada (scores 0-600 for all skills)
|
|
24
|
+
- TCF Canada (scores 0-600 for reading/listening, 0-16 for writing/speaking)
|
|
25
|
+
|
|
26
|
+
Returns CLB levels (0-12) for each skill:
|
|
27
|
+
- listening, reading, writing, speaking
|
|
28
|
+
- overall: minimum of all four skills
|
|
29
|
+
|
|
30
|
+
Example usage:
|
|
31
|
+
{
|
|
32
|
+
"test_type": "ielts",
|
|
33
|
+
"listening": 7.5,
|
|
34
|
+
"reading": 7.0,
|
|
35
|
+
"writing": 6.5,
|
|
36
|
+
"speaking": 7.0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
{
|
|
41
|
+
"test_type": "ielts",
|
|
42
|
+
"listening": 9,
|
|
43
|
+
"reading": 8,
|
|
44
|
+
"writing": 7,
|
|
45
|
+
"speaking": 8,
|
|
46
|
+
"overall": 7
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Use this tool when:
|
|
50
|
+
- Converting client's language test scores to CLB for immigration eligibility
|
|
51
|
+
- Checking Express Entry CRS score requirements
|
|
52
|
+
- Verifying IRCC language requirements for various programs`;
|
|
53
|
+
export const CLBConvertMcpTool = {
|
|
54
|
+
name: 'clb_convert',
|
|
55
|
+
description: CLB_CONVERT_DESCRIPTION,
|
|
56
|
+
inputShape: CLBConvertInputSchema,
|
|
57
|
+
outputSchema: CLBConvertOutputSchema,
|
|
58
|
+
annotations: {
|
|
59
|
+
title: 'CLB Score Converter',
|
|
60
|
+
readOnlyHint: true,
|
|
61
|
+
destructiveHint: false,
|
|
62
|
+
idempotentHint: true,
|
|
63
|
+
openWorldHint: false,
|
|
64
|
+
},
|
|
65
|
+
call: async (input) => {
|
|
66
|
+
try {
|
|
67
|
+
const result = await clbConvertHandler(input);
|
|
68
|
+
const text = [
|
|
69
|
+
`Test Type: ${result.test_type.toUpperCase()}`,
|
|
70
|
+
``,
|
|
71
|
+
`CLB Levels:`,
|
|
72
|
+
` Listening: CLB ${result.listening}`,
|
|
73
|
+
` Reading: CLB ${result.reading}`,
|
|
74
|
+
` Writing: CLB ${result.writing}`,
|
|
75
|
+
` Speaking: CLB ${result.speaking}`,
|
|
76
|
+
``,
|
|
77
|
+
`Overall CLB: ${result.overall}`,
|
|
78
|
+
].join('\n');
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: 'text', text }],
|
|
81
|
+
structuredContent: result,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to convert scores to CLB.';
|
|
86
|
+
console.error('Error in clb_convert tool call:', error);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { WageInfoSchema, NocWagesOutputSchema } from './noc_wages.tool.js';
|
|
3
|
+
export declare function nocWagesHandler(nocCode: string): Promise<z.infer<typeof NocWagesOutputSchema>>;
|
|
4
|
+
export declare function nocWagesByRegionHandler(nocCode: string, region: string): Promise<z.infer<typeof WageInfoSchema> | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { requireEnv } from '../common/tool-schema.js';
|
|
3
|
+
function getWagesApiUrl(nocCode) {
|
|
4
|
+
const HOST_URL = requireEnv('HOST_URL');
|
|
5
|
+
return `${HOST_URL}/tools/noc/${nocCode}/wages`;
|
|
6
|
+
}
|
|
7
|
+
function getWagesByRegionApiUrl(nocCode, region) {
|
|
8
|
+
const HOST_URL = requireEnv('HOST_URL');
|
|
9
|
+
return `${HOST_URL}/tools/noc/${nocCode}/wages/${encodeURIComponent(region)}`;
|
|
10
|
+
}
|
|
11
|
+
function getSystemToken() {
|
|
12
|
+
return requireEnv('SEARCH_SERVICE_TOKEN');
|
|
13
|
+
}
|
|
14
|
+
export async function nocWagesHandler(nocCode) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await axios.get(getWagesApiUrl(nocCode), {
|
|
17
|
+
timeout: 10000,
|
|
18
|
+
headers: { Authorization: `Bearer ${getSystemToken()}` },
|
|
19
|
+
});
|
|
20
|
+
return response.data;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (axios.isAxiosError(error)) {
|
|
24
|
+
console.error('Error calling NOC wages API:', error.message);
|
|
25
|
+
if (error.response) {
|
|
26
|
+
console.error('Response data:', error.response.data);
|
|
27
|
+
console.error('Response status:', error.response.status);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Failed to get wage data: ${error.response?.data?.detail || error.message}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.error('Unexpected error:', error);
|
|
33
|
+
throw new Error('An unexpected error occurred while retrieving wage data.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function nocWagesByRegionHandler(nocCode, region) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await axios.get(getWagesByRegionApiUrl(nocCode, region), {
|
|
40
|
+
timeout: 10000,
|
|
41
|
+
headers: { Authorization: `Bearer ${getSystemToken()}` },
|
|
42
|
+
});
|
|
43
|
+
return response.data;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (axios.isAxiosError(error)) {
|
|
47
|
+
if (error.response?.status === 404) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
console.error('Error calling NOC wages by region API:', error.message);
|
|
51
|
+
if (error.response) {
|
|
52
|
+
console.error('Response data:', error.response.data);
|
|
53
|
+
console.error('Response status:', error.response.status);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Failed to get wage data: ${error.response?.data?.detail || error.message}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.error('Unexpected error:', error);
|
|
59
|
+
throw new Error('An unexpected error occurred while retrieving wage data.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Tool } from '../common/tool-schema.js';
|
|
3
|
+
export declare const NocWagesInputSchema: z.ZodObject<{
|
|
4
|
+
noc_code: z.ZodString;
|
|
5
|
+
region: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
noc_code: string;
|
|
8
|
+
region?: string | undefined;
|
|
9
|
+
}, {
|
|
10
|
+
noc_code: string;
|
|
11
|
+
region?: string | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const WageInfoSchema: z.ZodObject<{
|
|
14
|
+
noc_code: z.ZodString;
|
|
15
|
+
region: z.ZodString;
|
|
16
|
+
low_wage: z.ZodNumber;
|
|
17
|
+
median_wage: z.ZodNumber;
|
|
18
|
+
high_wage: z.ZodNumber;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
noc_code: string;
|
|
21
|
+
region: string;
|
|
22
|
+
low_wage: number;
|
|
23
|
+
median_wage: number;
|
|
24
|
+
high_wage: number;
|
|
25
|
+
}, {
|
|
26
|
+
noc_code: string;
|
|
27
|
+
region: string;
|
|
28
|
+
low_wage: number;
|
|
29
|
+
median_wage: number;
|
|
30
|
+
high_wage: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare const NocWagesOutputSchema: z.ZodArray<z.ZodObject<{
|
|
33
|
+
noc_code: z.ZodString;
|
|
34
|
+
region: z.ZodString;
|
|
35
|
+
low_wage: z.ZodNumber;
|
|
36
|
+
median_wage: z.ZodNumber;
|
|
37
|
+
high_wage: z.ZodNumber;
|
|
38
|
+
}, "strip", z.ZodTypeAny, {
|
|
39
|
+
noc_code: string;
|
|
40
|
+
region: string;
|
|
41
|
+
low_wage: number;
|
|
42
|
+
median_wage: number;
|
|
43
|
+
high_wage: number;
|
|
44
|
+
}, {
|
|
45
|
+
noc_code: string;
|
|
46
|
+
region: string;
|
|
47
|
+
low_wage: number;
|
|
48
|
+
median_wage: number;
|
|
49
|
+
high_wage: number;
|
|
50
|
+
}>, "many">;
|
|
51
|
+
export declare const NOC_WAGES_DESCRIPTION = "Query wage information for National Occupational Classification (NOC) codes.\n\nReturns hourly wages (low, median, high) from the Government of Canada Job Bank data.\n\nParameters:\n- noc_code (required): NOC 2021 code (5 digits)\n- region (optional): Economic region name. If omitted, returns data for all available regions.\n\nExample usage - all regions:\n{\n \"noc_code\": \"21232\"\n}\n\nExample usage - specific region:\n{\n \"noc_code\": \"21232\",\n \"region\": \"Toronto\"\n}\n\nReturns wage data in CAD per hour:\n- low_wage: 10th percentile\n- median_wage: 50th percentile \n- high_wage: 90th percentile\n\nUse this tool when:\n- Checking LMIA wage requirements\n- Verifying prevailing wages for work permits\n- Comparing regional wage differences\n- Advising clients on salary expectations";
|
|
52
|
+
export declare const NocWagesMcpTool: Tool;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { nocWagesHandler, nocWagesByRegionHandler } from './noc_wages.handler.js';
|
|
3
|
+
export const NocWagesInputSchema = z.object({
|
|
4
|
+
noc_code: z.string().describe('NOC 2021 code (5 digits). Example: "21232" for Software Developers'),
|
|
5
|
+
region: z.string().optional().describe('Economic region name (optional). If not provided, returns wages for all regions. Example: "Toronto"'),
|
|
6
|
+
}).describe('Input for querying NOC wage information.');
|
|
7
|
+
export const WageInfoSchema = z.object({
|
|
8
|
+
noc_code: z.string().describe('NOC code'),
|
|
9
|
+
region: z.string().describe('Economic region name'),
|
|
10
|
+
low_wage: z.number().describe('Low wage (hourly, CAD)'),
|
|
11
|
+
median_wage: z.number().describe('Median wage (hourly, CAD)'),
|
|
12
|
+
high_wage: z.number().describe('High wage (hourly, CAD)'),
|
|
13
|
+
}).describe('Wage information for a NOC code in a specific region.');
|
|
14
|
+
export const NocWagesOutputSchema = z.array(WageInfoSchema).describe('List of wage information for the NOC code across regions.');
|
|
15
|
+
export const NOC_WAGES_DESCRIPTION = `Query wage information for National Occupational Classification (NOC) codes.
|
|
16
|
+
|
|
17
|
+
Returns hourly wages (low, median, high) from the Government of Canada Job Bank data.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- noc_code (required): NOC 2021 code (5 digits)
|
|
21
|
+
- region (optional): Economic region name. If omitted, returns data for all available regions.
|
|
22
|
+
|
|
23
|
+
Example usage - all regions:
|
|
24
|
+
{
|
|
25
|
+
"noc_code": "21232"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Example usage - specific region:
|
|
29
|
+
{
|
|
30
|
+
"noc_code": "21232",
|
|
31
|
+
"region": "Toronto"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Returns wage data in CAD per hour:
|
|
35
|
+
- low_wage: 10th percentile
|
|
36
|
+
- median_wage: 50th percentile
|
|
37
|
+
- high_wage: 90th percentile
|
|
38
|
+
|
|
39
|
+
Use this tool when:
|
|
40
|
+
- Checking LMIA wage requirements
|
|
41
|
+
- Verifying prevailing wages for work permits
|
|
42
|
+
- Comparing regional wage differences
|
|
43
|
+
- Advising clients on salary expectations`;
|
|
44
|
+
export const NocWagesMcpTool = {
|
|
45
|
+
name: 'noc_wages',
|
|
46
|
+
description: NOC_WAGES_DESCRIPTION,
|
|
47
|
+
inputShape: NocWagesInputSchema,
|
|
48
|
+
outputSchema: NocWagesOutputSchema,
|
|
49
|
+
annotations: {
|
|
50
|
+
title: 'NOC Wage Lookup',
|
|
51
|
+
readOnlyHint: true,
|
|
52
|
+
destructiveHint: false,
|
|
53
|
+
idempotentHint: true,
|
|
54
|
+
openWorldHint: true,
|
|
55
|
+
},
|
|
56
|
+
call: async (input) => {
|
|
57
|
+
try {
|
|
58
|
+
let results;
|
|
59
|
+
if (input.region) {
|
|
60
|
+
const result = await nocWagesByRegionHandler(input.noc_code, input.region);
|
|
61
|
+
results = result ? [result] : [];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
results = await nocWagesHandler(input.noc_code);
|
|
65
|
+
}
|
|
66
|
+
if (!results || results.length === 0) {
|
|
67
|
+
const msg = input.region
|
|
68
|
+
? `No wage data found for NOC ${input.noc_code} in ${input.region}`
|
|
69
|
+
: `No wage data found for NOC ${input.noc_code}`;
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text', text: msg }],
|
|
72
|
+
structuredContent: { results: [], total: 0 },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const text = results.map(w => [
|
|
76
|
+
`Region: ${w.region}`,
|
|
77
|
+
` Low: $${w.low_wage.toFixed(2)}/hr`,
|
|
78
|
+
` Median: $${w.median_wage.toFixed(2)}/hr`,
|
|
79
|
+
` High: $${w.high_wage.toFixed(2)}/hr`,
|
|
80
|
+
].join('\n')).join('\n\n');
|
|
81
|
+
const header = `NOC ${input.noc_code} Wage Data (${results.length} region${results.length > 1 ? 's' : ''})\n${'='.repeat(40)}\n\n`;
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: header + text }],
|
|
84
|
+
structuredContent: { results, total: results.length },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to retrieve wage data.';
|
|
89
|
+
console.error('Error in noc_wages tool call:', error);
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@immicore/immi-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for immigration tools (CLB conversion, NOC wage lookup)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"immicore-immi-tools": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "tsx src/index.ts --stdio",
|
|
18
|
+
"dev:http": "tsx src/index.ts --http --port 3009",
|
|
19
|
+
"dev:sse": "tsx src/index.ts --sse --port 3009",
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"start": "node dist/index.js --stdio",
|
|
23
|
+
"start:http": "node dist/index.js --http --port 3009",
|
|
24
|
+
"start:sse": "node dist/index.js --sse --port 3009"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
28
|
+
"axios": "^1.8.4",
|
|
29
|
+
"dotenv": "^17.2.3",
|
|
30
|
+
"express": "^4.21.2",
|
|
31
|
+
"zod": "^3.22.4"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/express": "^5.0.0",
|
|
35
|
+
"@types/node": "^22.15.2",
|
|
36
|
+
"tsx": "^4.16.2",
|
|
37
|
+
"typescript": "^5.4.5"
|
|
38
|
+
}
|
|
39
|
+
}
|