telique-mcp 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.
@@ -0,0 +1,14 @@
1
+ import type { Config } from "./config.js";
2
+ export declare class TeliqueClient {
3
+ private baseUrl;
4
+ private apiToken;
5
+ private timeoutMs;
6
+ get isAnonymous(): boolean;
7
+ constructor(config: Config);
8
+ get(path: string, params?: Record<string, string | number | undefined>): Promise<unknown>;
9
+ post(path: string, body: unknown): Promise<unknown>;
10
+ private buildUrl;
11
+ private request;
12
+ private tryParseJson;
13
+ private describeHttpError;
14
+ }
package/dist/client.js ADDED
@@ -0,0 +1,106 @@
1
+ export class TeliqueClient {
2
+ baseUrl;
3
+ apiToken;
4
+ timeoutMs;
5
+ get isAnonymous() {
6
+ return this.apiToken === null;
7
+ }
8
+ constructor(config) {
9
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
10
+ this.apiToken = config.apiToken;
11
+ this.timeoutMs = config.requestTimeoutMs;
12
+ }
13
+ async get(path, params) {
14
+ const url = this.buildUrl(path, params);
15
+ return this.request(url, { method: "GET" });
16
+ }
17
+ async post(path, body) {
18
+ const url = this.buildUrl(path);
19
+ return this.request(url, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify(body),
23
+ });
24
+ }
25
+ buildUrl(path, params) {
26
+ const url = new URL(path, this.baseUrl);
27
+ if (params) {
28
+ for (const [key, value] of Object.entries(params)) {
29
+ if (value !== undefined) {
30
+ url.searchParams.set(key, String(value));
31
+ }
32
+ }
33
+ }
34
+ return url.toString();
35
+ }
36
+ async request(url, init) {
37
+ const controller = new AbortController();
38
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
39
+ try {
40
+ const response = await fetch(url, {
41
+ ...init,
42
+ signal: controller.signal,
43
+ headers: {
44
+ ...(init.headers || {}),
45
+ ...(this.apiToken ? { "x-api-token": this.apiToken } : {}),
46
+ Accept: "application/json",
47
+ },
48
+ });
49
+ const text = await response.text();
50
+ if (!response.ok) {
51
+ return {
52
+ _error: true,
53
+ status: response.status,
54
+ message: this.describeHttpError(response.status),
55
+ body: this.tryParseJson(text),
56
+ };
57
+ }
58
+ return this.tryParseJson(text);
59
+ }
60
+ catch (err) {
61
+ if (err instanceof DOMException && err.name === "AbortError") {
62
+ return {
63
+ _error: true,
64
+ status: 0,
65
+ message: `Request timed out after ${this.timeoutMs}ms`,
66
+ };
67
+ }
68
+ return {
69
+ _error: true,
70
+ status: 0,
71
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`,
72
+ };
73
+ }
74
+ finally {
75
+ clearTimeout(timeout);
76
+ }
77
+ }
78
+ tryParseJson(text) {
79
+ try {
80
+ return JSON.parse(text);
81
+ }
82
+ catch {
83
+ return text;
84
+ }
85
+ }
86
+ describeHttpError(status) {
87
+ switch (status) {
88
+ case 400:
89
+ return "Bad request — check parameter format";
90
+ case 401:
91
+ case 403:
92
+ return "Authentication failed — check TELIQUE_API_TOKEN";
93
+ case 404:
94
+ return "Not found";
95
+ case 429:
96
+ return this.apiToken
97
+ ? "Rate limit exceeded"
98
+ : "Rate limit exceeded (10 ops/min in anonymous mode). Run `npx telique-mcp setup` or visit https://telique.ringer.tel to get an API key for unlimited access.";
99
+ case 502:
100
+ case 503:
101
+ return "Service temporarily unavailable";
102
+ default:
103
+ return `HTTP ${status}`;
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,9 @@
1
+ export interface Config {
2
+ baseUrl: string;
3
+ apiToken: string | null;
4
+ requestTimeoutMs: number;
5
+ }
6
+ declare const CONFIG_DIR: string;
7
+ declare const CONFIG_FILE: string;
8
+ export { CONFIG_DIR, CONFIG_FILE };
9
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CONFIG_DIR = join(homedir(), ".telique");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ export { CONFIG_DIR, CONFIG_FILE };
7
+ function loadTokenFromConfigFile() {
8
+ try {
9
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
10
+ const config = JSON.parse(raw);
11
+ return typeof config.apiToken === "string" ? config.apiToken : null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function loadConfig() {
18
+ // Token resolution: env var > config file > null (anonymous)
19
+ const apiToken = process.env.TELIQUE_API_TOKEN || loadTokenFromConfigFile() || null;
20
+ return {
21
+ baseUrl: process.env.TELIQUE_API_BASE_URL || "https://api-dev.ringer.tel",
22
+ apiToken,
23
+ requestTimeoutMs: parseInt(process.env.TELIQUE_REQUEST_TIMEOUT_MS || "10000", 10),
24
+ };
25
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from "./config.js";
3
+ const subcommand = process.argv[2];
4
+ if (subcommand === "setup") {
5
+ const { runSetup } = await import("./setup.js");
6
+ await runSetup();
7
+ }
8
+ else {
9
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
10
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
11
+ const { TeliqueClient } = await import("./client.js");
12
+ const { setAnonymousMode } = await import("./utils/formatting.js");
13
+ const { registerRoutelinkTools } = await import("./tools/routelink.js");
14
+ const { registerLrnTools } = await import("./tools/lrn.js");
15
+ const { registerCnamTools } = await import("./tools/cnam.js");
16
+ const { registerLergTools } = await import("./tools/lerg.js");
17
+ const { registerGraphqlTools } = await import("./tools/graphql.js");
18
+ const { registerCompositeTools } = await import("./tools/composite.js");
19
+ const config = loadConfig();
20
+ const client = new TeliqueClient(config);
21
+ setAnonymousMode(client.isAnonymous);
22
+ const server = new McpServer({
23
+ name: "telique",
24
+ version: "1.0.0",
25
+ });
26
+ registerRoutelinkTools(server, client);
27
+ registerLrnTools(server, client);
28
+ registerCnamTools(server, client);
29
+ registerLergTools(server, client);
30
+ registerGraphqlTools(server, client);
31
+ registerCompositeTools(server, client);
32
+ const transport = new StdioServerTransport();
33
+ await server.connect(transport);
34
+ }
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;
package/dist/setup.js ADDED
@@ -0,0 +1,131 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
3
+ import { stdin, stdout } from "node:process";
4
+ import { CONFIG_DIR, CONFIG_FILE } from "./config.js";
5
+ const REGISTER_URL = "https://telique.ringer.tel/register";
6
+ const API_BASE_URL = "https://api-dev.ringer.tel";
7
+ export async function runSetup() {
8
+ const rl = createInterface({ input: stdin, output: stdout });
9
+ console.log("\n Telique MCP — Setup\n");
10
+ // Check for existing config
11
+ const existing = loadExistingToken();
12
+ if (existing) {
13
+ console.log(` Found existing API key: ${maskToken(existing)}`);
14
+ const keep = await rl.question(" Keep this key? (Y/n): ");
15
+ if (keep.toLowerCase() !== "n") {
16
+ console.log("\n ✓ Keeping existing configuration.\n");
17
+ printMcpConfig(existing);
18
+ rl.close();
19
+ return;
20
+ }
21
+ }
22
+ console.log(" Do you have an API key?\n");
23
+ console.log(" [1] Yes, I have one → Enter it");
24
+ console.log(" [2] No, I need one → Opens telique.ringer.tel in browser");
25
+ console.log(" [3] Skip for now → Use anonymous mode (10 ops/min)");
26
+ console.log();
27
+ const choice = await rl.question(" > ");
28
+ switch (choice.trim()) {
29
+ case "1": {
30
+ const token = await rl.question("\n Enter your API key: ");
31
+ const trimmed = token.trim();
32
+ if (!trimmed) {
33
+ console.log("\n ✗ No key entered. Exiting.\n");
34
+ rl.close();
35
+ process.exit(1);
36
+ }
37
+ console.log("\n Validating...");
38
+ const valid = await validateToken(trimmed);
39
+ if (!valid) {
40
+ console.log(" ✗ Token validation failed. The API returned an error.");
41
+ console.log(" Check your token and try again.\n");
42
+ rl.close();
43
+ process.exit(1);
44
+ }
45
+ saveToken(trimmed);
46
+ console.log(` ✓ Token validated`);
47
+ console.log(` ✓ Saved to ${CONFIG_FILE}\n`);
48
+ printMcpConfig(trimmed);
49
+ break;
50
+ }
51
+ case "2": {
52
+ console.log(`\n Opening ${REGISTER_URL} ...\n`);
53
+ await openBrowser(REGISTER_URL);
54
+ console.log(" After creating your account, run this command again with your API key.\n");
55
+ break;
56
+ }
57
+ case "3":
58
+ default: {
59
+ console.log("\n ✓ Skipped. Running in anonymous mode (10 ops/min).");
60
+ console.log(` Get an API key anytime at ${REGISTER_URL}\n`);
61
+ printMcpConfig(null);
62
+ break;
63
+ }
64
+ }
65
+ rl.close();
66
+ }
67
+ function loadExistingToken() {
68
+ try {
69
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
70
+ const config = JSON.parse(raw);
71
+ return typeof config.apiToken === "string" ? config.apiToken : null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ function saveToken(token) {
78
+ mkdirSync(CONFIG_DIR, { recursive: true });
79
+ const config = existsSync(CONFIG_FILE)
80
+ ? JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
81
+ : {};
82
+ config.apiToken = token;
83
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
84
+ }
85
+ async function validateToken(token) {
86
+ try {
87
+ const response = await fetch(`${API_BASE_URL}/health`, {
88
+ headers: { "x-api-token": token },
89
+ signal: AbortSignal.timeout(10000),
90
+ });
91
+ return response.ok;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ function maskToken(token) {
98
+ if (token.length <= 8)
99
+ return "****";
100
+ return token.substring(0, 4) + "..." + token.substring(token.length - 4);
101
+ }
102
+ async function openBrowser(url) {
103
+ const { exec } = await import("node:child_process");
104
+ const cmd = process.platform === "darwin"
105
+ ? "open"
106
+ : process.platform === "win32"
107
+ ? "start"
108
+ : "xdg-open";
109
+ exec(`${cmd} ${url}`);
110
+ }
111
+ function printMcpConfig(token) {
112
+ const env = {};
113
+ if (token) {
114
+ env.TELIQUE_API_TOKEN = token;
115
+ }
116
+ const config = {
117
+ mcpServers: {
118
+ telique: {
119
+ command: "npx",
120
+ args: ["-y", "telique-mcp"],
121
+ ...(token ? { env } : {}),
122
+ },
123
+ },
124
+ };
125
+ console.log(" Add this to your MCP client configuration:\n");
126
+ console.log(JSON.stringify(config, null, 2)
127
+ .split("\n")
128
+ .map((line) => " " + line)
129
+ .join("\n"));
130
+ console.log();
131
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "../client.js";
3
+ export declare function registerCnamTools(server: McpServer, client: TeliqueClient): void;
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { formatResponse } from "../utils/formatting.js";
3
+ export function registerCnamTools(server, client) {
4
+ server.tool("cnam_lookup", "Look up the Caller Name (CNAM) for a phone number via TransUnion LIDB. Returns the calling_name (up to 15 characters), calling_name_status (available/unavailable), and presentation_indicator (allowed/restricted). Results are cached server-side for 24 hours.", {
5
+ phone_number: z
6
+ .string()
7
+ .regex(/^\d{10,15}$/)
8
+ .describe("10-15 digit phone number to look up"),
9
+ }, async ({ phone_number }) => {
10
+ const result = await client.get(`/v1/telique/cnam/${phone_number}`);
11
+ return formatResponse(result);
12
+ });
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "../client.js";
3
+ export declare function registerCompositeTools(server: McpServer, client: TeliqueClient): void;
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { formatResponse } from "../utils/formatting.js";
3
+ export function registerCompositeTools(server, client) {
4
+ server.tool("lookup_tn", "Composite phone number lookup across multiple Telique services. Dips LRN, CNAM, DNO, and LERG (NPA-NXX from lerg_6) in parallel and returns a consolidated view. Use this for a quick, comprehensive profile of any phone number.", {
5
+ phone_number: z
6
+ .string()
7
+ .regex(/^\d{10}$/)
8
+ .describe("10-digit US phone number"),
9
+ }, async ({ phone_number }) => {
10
+ const npa = phone_number.substring(0, 3);
11
+ const nxx = phone_number.substring(3, 6);
12
+ const [lrn, cnam, dno, lerg] = await Promise.all([
13
+ client
14
+ .get(`/v1/telique/lrn/${phone_number}`, { format: "json" })
15
+ .catch((err) => ({ _error: true, message: err.message })),
16
+ client
17
+ .get(`/v1/telique/cnam/${phone_number}`)
18
+ .catch((err) => ({ _error: true, message: err.message })),
19
+ client
20
+ .get(`/v1/telique/dno/${phone_number}`, { format: "json" })
21
+ .catch((err) => ({ _error: true, message: err.message })),
22
+ client
23
+ .get(`/v1/telique/lerg/lerg_6/npa,nxx,loc_name,loc_state,lata,lata_name,ocn,switch,rc_abbre,rc_type/npa=${npa}%26nxx=${nxx}`, { limit: 5 })
24
+ .catch((err) => ({ _error: true, message: err.message })),
25
+ ]);
26
+ const consolidated = {
27
+ phone_number,
28
+ npa,
29
+ nxx,
30
+ lrn,
31
+ cnam,
32
+ dno,
33
+ lerg_6: lerg,
34
+ };
35
+ return formatResponse(consolidated);
36
+ });
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "../client.js";
3
+ export declare function registerGraphqlTools(server: McpServer, client: TeliqueClient): void;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { formatResponse } from "../utils/formatting.js";
3
+ export function registerGraphqlTools(server, client) {
4
+ server.tool("graphql_query", `Execute GraphQL queries against LSMS or LERG databases.
5
+
6
+ LSMS (service='lsms'): Live NPAC porting data. Tables: subscriptionVersions (514M rows — MUST filter by phone_number, lrn, or spid), numberBlocks, serviceProviders, locationRoutingNumbers, npanxx. Safety limits: max 1000 results, depth 5, complexity 200, 10s timeout.
7
+
8
+ LERG (service='lerg'): Static telecom reference data. All 27 LERG tables with camelCase names (lerg1, lerg6, lerg7Sha, etc.). Supports relationship joins: lerg6→carrier (via OCN→lerg1), lerg6→switchInfo (→lerg7), lerg7Sha→tandemSwitch. Filter operators: EQ, NE, GT, GTE, LT, LTE, LIKE, IN, IS_NULL, IS_NOT_NULL. All field values are nullable strings.`, {
9
+ service: z
10
+ .enum(["lsms", "lerg"])
11
+ .describe("Target service: 'lsms' for live porting data, 'lerg' for static telecom reference"),
12
+ query: z.string().describe("GraphQL query string"),
13
+ variables: z
14
+ .record(z.unknown())
15
+ .optional()
16
+ .describe("Optional GraphQL variables"),
17
+ }, async ({ service, query, variables }) => {
18
+ const path = service === "lsms"
19
+ ? "/v1/telique/lsms/gql"
20
+ : "/v1/telique/lerg/gql";
21
+ const body = { query };
22
+ if (variables)
23
+ body.variables = variables;
24
+ const result = await client.post(path, body);
25
+ return formatResponse(result);
26
+ });
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "../client.js";
3
+ export declare function registerLergTools(server: McpServer, client: TeliqueClient): void;
@@ -0,0 +1,151 @@
1
+ import { z } from "zod";
2
+ import { formatResponse } from "../utils/formatting.js";
3
+ const LERG_TABLES = [
4
+ "lerg_1",
5
+ "lerg_1_con",
6
+ "lerg_2",
7
+ "lerg_3",
8
+ "lerg_4",
9
+ "lerg_5",
10
+ "lerg_6",
11
+ "lerg_6_atc",
12
+ "lerg_6_ins",
13
+ "lerg_6_odd",
14
+ "lerg_7",
15
+ "lerg_7_ins",
16
+ "lerg_7_sha",
17
+ "lerg_7_sha_ins",
18
+ "lerg_8",
19
+ "lerg_8_lir",
20
+ "lerg_8_loc",
21
+ "lerg_8_pst",
22
+ "lerg_9",
23
+ "lerg_9_atc",
24
+ "lerg_10",
25
+ "lerg_11",
26
+ "lerg_12",
27
+ "lerg_12_ins",
28
+ "lerg_16",
29
+ "lerg_17",
30
+ "lergdate",
31
+ ];
32
+ export function registerLergTools(server, client) {
33
+ server.tool("lerg_table_info", "List all 27 LERG tables or get metadata/schema for a specific table. LERG is static telecom reference data. Key tables: lerg_1 (OCN/carrier directory), lerg_6 (NPA-NXX block assignments with switch, LATA, rate center), lerg_7 (switch details), lerg_7_sha (switch homing arrangements/tandems), lerg_12 (LRN registry).", {
34
+ table_name: z
35
+ .string()
36
+ .optional()
37
+ .describe("Specific table name (e.g. lerg_1, lerg_6, lerg_7_sha). Omit to list all tables."),
38
+ }, async ({ table_name }) => {
39
+ if (table_name) {
40
+ const result = await client.get(`/v1/telique/lerg/tables/${table_name}`);
41
+ return formatResponse(result);
42
+ }
43
+ const result = await client.get("/v1/telique/lerg/tables");
44
+ return formatResponse(result);
45
+ });
46
+ server.tool("lerg_query", "Query any LERG table by field values. Common queries: carrier by OCN (lerg_1, fields: ocn_num,ocn_name,ocn_state), NPA-NXX info (lerg_6, fields: npa,nxx,loc_name,ocn,switch,lata), switch details (lerg_7, fields: switch,ocn,aocn), LRN registry (lerg_12, fields: lrn,lata,switch,ocn). Filter format: field=value, multiple filters joined with & (e.g. npa=303&nxx=629).", {
47
+ table_name: z.string().describe("Table to query (e.g. lerg_1, lerg_6)"),
48
+ fields: z
49
+ .string()
50
+ .describe("Comma-separated field names to return (e.g. ocn_num,ocn_name,ocn_state)"),
51
+ query: z
52
+ .string()
53
+ .describe("Filter in field=value format, multiple filters joined with & (e.g. ocn_state=CO or npa=303&nxx=629)"),
54
+ limit: z
55
+ .number()
56
+ .int()
57
+ .min(1)
58
+ .max(10000)
59
+ .default(100)
60
+ .describe("Max results (default 100, max 10000)"),
61
+ offset: z
62
+ .number()
63
+ .int()
64
+ .min(0)
65
+ .default(0)
66
+ .describe("Pagination offset (default 0)"),
67
+ }, async ({ table_name, fields, query, limit, offset }) => {
68
+ // Encode & as %26 so multi-filters stay in the path segment
69
+ const encodedQuery = query.replace(/&/g, "%26");
70
+ const result = await client.get(`/v1/telique/lerg/${table_name}/${fields}/${encodedQuery}`, { limit, offset });
71
+ return formatResponse(result);
72
+ });
73
+ server.tool("lerg_complex_query", "Execute a complex LERG query with JOINs across multiple tables. Supports filter operators: eq, ne, gt, gte, lt, lte, like, in, isnull, isnotnull. Use this when you need to combine data from different LERG tables, such as joining NPA-NXX (lerg_6) with carrier info (lerg_1) via OCN.", {
74
+ table: z.string().describe("Primary table name (e.g. lerg_6)"),
75
+ fields: z
76
+ .array(z.string())
77
+ .optional()
78
+ .describe("Fields to return from primary table (e.g. ['npa','nxx','ocn','loc_name'])"),
79
+ filters: z
80
+ .array(z.object({
81
+ field: z.string(),
82
+ operator: z.enum([
83
+ "eq",
84
+ "ne",
85
+ "gt",
86
+ "gte",
87
+ "lt",
88
+ "lte",
89
+ "like",
90
+ "in",
91
+ "isnull",
92
+ "isnotnull",
93
+ ]),
94
+ value: z.union([z.string(), z.number()]),
95
+ }))
96
+ .describe("Filter conditions (e.g. [{field:'npa', operator:'eq', value:720}])"),
97
+ join: z
98
+ .object({
99
+ table: z.string().describe("Table to join (e.g. lerg_1)"),
100
+ on: z
101
+ .array(z.object({
102
+ left_field: z.string(),
103
+ right_field: z.string(),
104
+ }))
105
+ .describe("Join conditions (e.g. [{left_field:'ocn', right_field:'ocn_num'}])"),
106
+ fields: z
107
+ .array(z.string())
108
+ .optional()
109
+ .describe("Fields to return from joined table"),
110
+ join_type: z
111
+ .enum(["inner", "left"])
112
+ .default("inner")
113
+ .describe("Join type"),
114
+ })
115
+ .optional()
116
+ .describe("Optional JOIN clause"),
117
+ limit: z
118
+ .number()
119
+ .int()
120
+ .min(1)
121
+ .max(10000)
122
+ .default(100)
123
+ .describe("Max results (default 100)"),
124
+ offset: z
125
+ .number()
126
+ .int()
127
+ .min(0)
128
+ .default(0)
129
+ .describe("Pagination offset (default 0)"),
130
+ }, async ({ table, fields, filters, join, limit, offset }) => {
131
+ const body = {
132
+ table,
133
+ filters,
134
+ limit,
135
+ offset,
136
+ };
137
+ if (fields)
138
+ body.fields = fields;
139
+ if (join)
140
+ body.join = join;
141
+ const result = await client.post("/v1/telique/lerg/query", body);
142
+ return formatResponse(result);
143
+ });
144
+ server.tool("lerg_tandem", "Look up tandem routing information for a given NPA-NXX. Returns the tandem switch and routing path for calls to a specific area code and exchange. Uses SQL JOINs across LERG tables (lerg_6, lerg_7, lerg_7_sha) for comprehensive routing data.", {
145
+ npa: z.string().regex(/^\d{3}$/).describe("3-digit area code (NPA)"),
146
+ nxx: z.string().regex(/^\d{3}$/).describe("3-digit exchange code (NXX)"),
147
+ }, async ({ npa, nxx }) => {
148
+ const result = await client.get("/v1/telique/lerg/tandem", { npa, nxx });
149
+ return formatResponse(result);
150
+ });
151
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "../client.js";
3
+ export declare function registerLrnTools(server: McpServer, client: TeliqueClient): void;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { formatResponse, errorResult } from "../utils/formatting.js";
3
+ export function registerLrnTools(server, client) {
4
+ server.tool("lrn_lookup", "Look up the Local Routing Number (LRN) for a phone number. Returns the LRN, SPID (Service Provider ID), LNP type, and activation timestamp. LRN identifies the switch that serves a ported phone number. This queries live LSMS/NPAC porting data.", {
5
+ phone_number: z
6
+ .string()
7
+ .regex(/^\d{10}$/)
8
+ .describe("10-digit US phone number"),
9
+ }, async ({ phone_number }) => {
10
+ const result = await client.get(`/v1/telique/lrn/${phone_number}`, {
11
+ format: "json",
12
+ });
13
+ return formatResponse(result);
14
+ });
15
+ server.tool("lrn_relationship_query", "Query relationships in the LSMS database. Find phone numbers by LRN, SPIDs by LRN or phone number, or LRNs by SPID or phone number. Queries live NPAC porting data (not static LERG reference data).", {
16
+ query_type: z
17
+ .enum([
18
+ "phones_by_lrn",
19
+ "phones_by_spid",
20
+ "spid_by_lrn",
21
+ "spid_by_phone",
22
+ "lrn_by_spid",
23
+ "lrn_by_phone",
24
+ ])
25
+ .describe("Type of relationship query"),
26
+ value: z
27
+ .string()
28
+ .describe("The LRN, SPID, or phone number to query by (depends on query_type)"),
29
+ }, async ({ query_type, value }) => {
30
+ const routeMap = {
31
+ phones_by_lrn: { resource: "phone_number", param: "lrn" },
32
+ phones_by_spid: { resource: "phone_number", param: "spid" },
33
+ spid_by_lrn: { resource: "spid", param: "lrn" },
34
+ spid_by_phone: { resource: "spid", param: "phone_number" },
35
+ lrn_by_spid: { resource: "lrn", param: "spid" },
36
+ lrn_by_phone: { resource: "lrn", param: "phone_number" },
37
+ };
38
+ const route = routeMap[query_type];
39
+ if (!route) {
40
+ return errorResult(`Unknown query_type: ${query_type}`);
41
+ }
42
+ const result = await client.get(`/v1/telique/lsms/list/${route.resource}`, { [route.param]: value });
43
+ return formatResponse(result);
44
+ });
45
+ server.tool("dno_check", "Check if a phone number is on the Do Not Originate (DNO) list. DNO numbers should never appear as a caller ID because they belong to entities that only receive calls (e.g., IRS, major banks). A match indicates potential caller ID spoofing. Supports prefix matching (3, 6, 7, or 10 digit patterns).", {
46
+ phone_number: z
47
+ .string()
48
+ .regex(/^\d{10}$/)
49
+ .describe("10-digit US phone number to check"),
50
+ }, async ({ phone_number }) => {
51
+ const result = await client.get(`/v1/telique/dno/${phone_number}`, {
52
+ format: "json",
53
+ });
54
+ return formatResponse(result);
55
+ });
56
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "../client.js";
3
+ export declare function registerRoutelinkTools(server: McpServer, client: TeliqueClient): void;
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { formatResponse, errorResult } from "../utils/formatting.js";
3
+ export function registerRoutelinkTools(server, client) {
4
+ server.tool("routelink_lookup", "Look up toll-free number routing. Resolves a CRN (toll-free number) to its carrier (CIC), Responsible Organization (ROR), or both. CIC lookup interprets the CPR decision tree using the caller's ANI and LATA to determine which carrier handles the call.", {
5
+ crn: z
6
+ .string()
7
+ .regex(/^\d{10}$/)
8
+ .describe("10-digit toll-free number (CRN), e.g. 8005551234"),
9
+ lookup_type: z
10
+ .enum(["cic", "cicror", "ror"])
11
+ .describe("Type of lookup: 'cic' = carrier ID, 'ror' = responsible org, 'cicror' = both"),
12
+ ani: z
13
+ .string()
14
+ .regex(/^\d{10}$/)
15
+ .optional()
16
+ .describe("10-digit calling party number (required for cic and cicror lookups)"),
17
+ lata: z
18
+ .string()
19
+ .regex(/^\d{3}$/)
20
+ .optional()
21
+ .describe("3-digit LATA code (required for cic and cicror lookups)"),
22
+ }, async ({ crn, lookup_type, ani, lata }) => {
23
+ if ((lookup_type === "cic" || lookup_type === "cicror") &&
24
+ (!ani || !lata)) {
25
+ return errorResult("ani and lata are required for cic and cicror lookups");
26
+ }
27
+ let path;
28
+ if (lookup_type === "ror") {
29
+ path = `/v1/telique/ror/${crn}`;
30
+ }
31
+ else {
32
+ path = `/v1/telique/${lookup_type}/${crn}/${ani}/${lata}`;
33
+ }
34
+ const result = await client.get(path, { format: "json" });
35
+ return formatResponse(result);
36
+ });
37
+ server.tool("routelink_ror_query", "List toll-free numbers (TFNs) or Call Processing Records (CPRs) associated with a Responsible Organization (ROR). Use this to explore which toll-free numbers a specific organization manages.", {
38
+ ror: z
39
+ .string()
40
+ .regex(/^[A-Za-z0-9]{1,5}$/)
41
+ .describe("1-5 character alphanumeric ROR code (e.g. ATX01, CTJLE)"),
42
+ resource_type: z
43
+ .enum(["tfns", "cprs"])
44
+ .describe("Whether to list TFNs or CPRs for this ROR"),
45
+ limit: z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .max(10000)
50
+ .default(100)
51
+ .describe("Max results to return (default 100, max 10000)"),
52
+ offset: z
53
+ .number()
54
+ .int()
55
+ .min(0)
56
+ .default(0)
57
+ .describe("Pagination offset (default 0)"),
58
+ }, async ({ ror, resource_type, limit, offset }) => {
59
+ const result = await client.get(`/v1/telique/ror/${ror}/${resource_type}`, { format: "json", limit, offset });
60
+ return formatResponse(result);
61
+ });
62
+ server.tool("routelink_cpr", "Retrieve the full Call Processing Record (CPR) for a toll-free number. A CPR is a routing decision tree that determines how calls are routed based on LATA, NPA, NXX, ANI, DAY_OF_WEEK, TIME_OF_DAY, PERCENT, STATE, etc. Returns the CPR structure, SHA1 hash, ROR, and optionally expanded template references.", {
63
+ crn: z
64
+ .string()
65
+ .regex(/^\d{1,10}$/)
66
+ .describe("1-10 digit CRN (toll-free number or template CRN). Shorter values are zero-padded to 10 digits."),
67
+ expand: z
68
+ .boolean()
69
+ .default(true)
70
+ .describe("Recursively resolve and inline template decision trees (default true)"),
71
+ }, async ({ crn, expand }) => {
72
+ // NOTE: /v1/telique/cpr/* path pending frontend URL map addition.
73
+ // Falls back to /cpr/ which routes to routelink via default backend.
74
+ const result = await client.get(`/v1/telique/cpr/${crn}`, {
75
+ format: "json",
76
+ expand: expand ? "true" : "false",
77
+ });
78
+ return formatResponse(result);
79
+ });
80
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TeliqueClient } from "./client.js";
3
+ export type ToolRegistrar = (server: McpServer, client: TeliqueClient) => void;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export declare function setAnonymousMode(isAnonymous: boolean): void;
2
+ export declare function formatResponse(data: unknown): {
3
+ content: Array<{
4
+ type: "text";
5
+ text: string;
6
+ }>;
7
+ isError?: boolean;
8
+ };
9
+ export declare function errorResult(message: string): {
10
+ content: {
11
+ type: "text";
12
+ text: string;
13
+ }[];
14
+ isError: boolean;
15
+ };
@@ -0,0 +1,67 @@
1
+ const MAX_ITEMS = 50;
2
+ const ANONYMOUS_NOTICE = "\n\n---\n⚠ Anonymous mode (10 ops/min). Run `npx telique-mcp setup` or visit https://telique.ringer.tel for unlimited access.";
3
+ let anonymous = false;
4
+ let noticeShown = false;
5
+ export function setAnonymousMode(isAnonymous) {
6
+ anonymous = isAnonymous;
7
+ }
8
+ function isErrorResponse(value) {
9
+ return (typeof value === "object" &&
10
+ value !== null &&
11
+ "_error" in value &&
12
+ value._error === true);
13
+ }
14
+ export function formatResponse(data) {
15
+ if (isErrorResponse(data)) {
16
+ const parts = [`Error: ${data.message}`];
17
+ if (data.body && typeof data.body === "object") {
18
+ parts.push(JSON.stringify(data.body, null, 2));
19
+ }
20
+ else if (data.body) {
21
+ parts.push(String(data.body));
22
+ }
23
+ return {
24
+ content: [{ type: "text", text: parts.join("\n\n") }],
25
+ isError: true,
26
+ };
27
+ }
28
+ const truncated = truncateArrays(data);
29
+ let text = JSON.stringify(truncated, null, 2);
30
+ if (anonymous && !noticeShown) {
31
+ text += ANONYMOUS_NOTICE;
32
+ noticeShown = true;
33
+ }
34
+ return {
35
+ content: [{ type: "text", text }],
36
+ };
37
+ }
38
+ function truncateArrays(data) {
39
+ if (Array.isArray(data)) {
40
+ if (data.length > MAX_ITEMS) {
41
+ return {
42
+ _meta: {
43
+ total: data.length,
44
+ returned: MAX_ITEMS,
45
+ has_more: true,
46
+ message: `Showing ${MAX_ITEMS} of ${data.length} items. Use limit/offset to paginate.`,
47
+ },
48
+ items: data.slice(0, MAX_ITEMS),
49
+ };
50
+ }
51
+ return data.map(truncateArrays);
52
+ }
53
+ if (typeof data === "object" && data !== null) {
54
+ const result = {};
55
+ for (const [key, value] of Object.entries(data)) {
56
+ result[key] = truncateArrays(value);
57
+ }
58
+ return result;
59
+ }
60
+ return data;
61
+ }
62
+ export function errorResult(message) {
63
+ return {
64
+ content: [{ type: "text", text: `Error: ${message}` }],
65
+ isError: true,
66
+ };
67
+ }
@@ -0,0 +1,6 @@
1
+ export declare function isValidPhoneNumber(value: string): boolean;
2
+ export declare function isValidCrn(value: string): boolean;
3
+ export declare function isValidNpa(value: string): boolean;
4
+ export declare function isValidNxx(value: string): boolean;
5
+ export declare function isValidLata(value: string): boolean;
6
+ export declare function isValidRor(value: string): boolean;
@@ -0,0 +1,18 @@
1
+ export function isValidPhoneNumber(value) {
2
+ return /^\d{10,15}$/.test(value);
3
+ }
4
+ export function isValidCrn(value) {
5
+ return /^\d{1,10}$/.test(value);
6
+ }
7
+ export function isValidNpa(value) {
8
+ return /^\d{3}$/.test(value);
9
+ }
10
+ export function isValidNxx(value) {
11
+ return /^\d{3}$/.test(value);
12
+ }
13
+ export function isValidLata(value) {
14
+ return /^\d{3}$/.test(value);
15
+ }
16
+ export function isValidRor(value) {
17
+ return /^[A-Za-z0-9]{1,5}$/.test(value);
18
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "telique-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Telique telecom APIs (RouteLink, LRN, CNAM, LERG)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "telique-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "dev": "tsx src/index.ts",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "telique",
23
+ "telecom",
24
+ "lrn",
25
+ "cnam",
26
+ "lerg",
27
+ "routelink"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Ringer/telique-mcp.git"
32
+ },
33
+ "license": "UNLICENSED",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.12.0",
36
+ "zod": "^3.23.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "tsx": "^4.19.0",
41
+ "typescript": "^5.7.0"
42
+ }
43
+ }