@vicinitysoftware/mcp-sql 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import * as sql from "mssql";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as url from "url";
9
+ import * as dotenv from "dotenv";
10
+ dotenv.config();
11
+ // ── Database config ───────────────────────────────────────────────────────────
12
+ const dbConfig = {
13
+ server: process.env.VICINITY_SQL_SERVER,
14
+ database: process.env.VICINITY_SQL_DATABASE,
15
+ user: process.env.VICINITY_SQL_USER,
16
+ password: process.env.VICINITY_SQL_PASSWORD,
17
+ port: parseInt(process.env.VICINITY_SQL_PORT || "1433"),
18
+ options: {
19
+ encrypt: process.env.VICINITY_SQL_ENCRYPT === "true",
20
+ trustServerCertificate: process.env.VICINITY_SQL_TRUST_SERVER_CERT !== "false",
21
+ connectTimeout: 15000,
22
+ requestTimeout: parseInt(process.env.VICINITY_SQL_TIMEOUT || "30000"),
23
+ },
24
+ pool: { max: 5, min: 0, idleTimeoutMillis: 30000 },
25
+ };
26
+ const MAX_ROWS = parseInt(process.env.VICINITY_SQL_MAX_ROWS || "500");
27
+ let pool;
28
+ async function getPool() {
29
+ if (!pool) {
30
+ pool = await sql.connect(dbConfig);
31
+ }
32
+ return pool;
33
+ }
34
+ async function runQuery(querySql, maxRows = 100) {
35
+ const trimmed = querySql.trim().toUpperCase();
36
+ if (!trimmed.startsWith("SELECT") && !trimmed.startsWith("WITH")) {
37
+ throw new Error("Only SELECT queries are permitted.");
38
+ }
39
+ const limit = Math.min(maxRows, MAX_ROWS);
40
+ const p = await getPool();
41
+ const result = await p.request().query(querySql);
42
+ const rows = result.recordset.slice(0, limit);
43
+ if (rows.length === 0)
44
+ return "Query returned no results.";
45
+ const columns = Object.keys(rows[0]);
46
+ const colWidths = columns.map(c => Math.max(c.length, ...rows.map(r => String(r[c] ?? "NULL").length)));
47
+ const fmt = (vals) => vals.map((v, i) => v.padEnd(colWidths[i])).join(" ");
48
+ const lines = [
49
+ fmt(columns),
50
+ colWidths.map(w => "-".repeat(w)).join("--"),
51
+ ...rows.map(r => fmt(columns.map(c => String(r[c] ?? "NULL")))),
52
+ `\n(${rows.length} rows${result.recordset.length > limit ? " — truncated" : ""})`,
53
+ ];
54
+ return lines.join("\n");
55
+ }
56
+ // ── Schema context loader ─────────────────────────────────────────────────────
57
+ function loadSchemaContext() {
58
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
59
+ const schemaFile = path.join(__dirname, "../resources/vicinity-schema-context.md");
60
+ const clientFile = path.join(__dirname, "../resources/vicinity-client-context.md");
61
+ if (!fs.existsSync(schemaFile)) {
62
+ return "Schema context file not found.";
63
+ }
64
+ let content = fs.readFileSync(schemaFile, "utf-8");
65
+ if (fs.existsSync(clientFile)) {
66
+ const clientContent = fs.readFileSync(clientFile, "utf-8");
67
+ content += "\n\n---\n\n## Client-Specific Context\n\n" + clientContent;
68
+ }
69
+ return content;
70
+ }
71
+ // ── MCP Server ────────────────────────────────────────────────────────────────
72
+ const server = new Server({ name: "vicinity-mcp-sql", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } });
73
+ // Resources
74
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
75
+ resources: [
76
+ {
77
+ uri: "vicinity://schema-context",
78
+ name: "Vicinity Database Schema & Business Context",
79
+ description: "Table relationships, business terminology, and query patterns for Vicinity manufacturing databases",
80
+ mimeType: "text/markdown",
81
+ },
82
+ ],
83
+ }));
84
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
85
+ if (request.params.uri === "vicinity://schema-context") {
86
+ return {
87
+ contents: [{
88
+ uri: request.params.uri,
89
+ mimeType: "text/markdown",
90
+ text: loadSchemaContext(),
91
+ }],
92
+ };
93
+ }
94
+ throw new Error(`Unknown resource: ${request.params.uri}`);
95
+ });
96
+ // Tools
97
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
98
+ tools: [
99
+ {
100
+ name: "list_tables",
101
+ description: "List all user tables in the Vicinity database.",
102
+ inputSchema: { type: "object", properties: {} },
103
+ },
104
+ {
105
+ name: "list_views",
106
+ description: "List all views in the Vicinity database.",
107
+ inputSchema: { type: "object", properties: {} },
108
+ },
109
+ {
110
+ name: "describe_table",
111
+ description: "Return column names, types, and nullability for a table or view.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ table_name: { type: "string", description: "Table or view name" },
116
+ schema: { type: "string", description: "Schema name (default: dbo)" },
117
+ },
118
+ required: ["table_name"],
119
+ },
120
+ },
121
+ {
122
+ name: "run_query",
123
+ description: "Execute a read-only SELECT query against the Vicinity database.",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ sql: { type: "string", description: "SQL SELECT statement to execute" },
128
+ max_rows: { type: "number", description: "Maximum rows to return (default 100, max 500)" },
129
+ },
130
+ required: ["sql"],
131
+ },
132
+ },
133
+ ],
134
+ }));
135
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
136
+ const { name, arguments: args } = request.params;
137
+ try {
138
+ let result = "";
139
+ if (name === "list_tables") {
140
+ const p = await getPool();
141
+ const res = await p.request().query(`
142
+ SELECT TABLE_SCHEMA, TABLE_NAME
143
+ FROM INFORMATION_SCHEMA.TABLES
144
+ WHERE TABLE_TYPE = 'BASE TABLE'
145
+ ORDER BY TABLE_SCHEMA, TABLE_NAME
146
+ `);
147
+ result = res.recordset.map((r) => `${r.TABLE_SCHEMA}.${r.TABLE_NAME}`).join("\n");
148
+ }
149
+ else if (name === "list_views") {
150
+ const p = await getPool();
151
+ const res = await p.request().query(`
152
+ SELECT TABLE_SCHEMA, TABLE_NAME
153
+ FROM INFORMATION_SCHEMA.VIEWS
154
+ ORDER BY TABLE_SCHEMA, TABLE_NAME
155
+ `);
156
+ result = res.recordset.map((r) => `${r.TABLE_SCHEMA}.${r.TABLE_NAME}`).join("\n") || "No views found.";
157
+ }
158
+ else if (name === "describe_table") {
159
+ const p = await getPool();
160
+ const res = await p.request()
161
+ .input("table", sql.NVarChar, args?.table_name)
162
+ .input("schema", sql.NVarChar, args?.schema ?? "dbo")
163
+ .query(`
164
+ SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH
165
+ FROM INFORMATION_SCHEMA.COLUMNS
166
+ WHERE TABLE_NAME = @table AND TABLE_SCHEMA = @schema
167
+ ORDER BY ORDINAL_POSITION
168
+ `);
169
+ if (res.recordset.length === 0) {
170
+ result = `Table '${args?.schema ?? "dbo"}.${args?.table_name}' not found.`;
171
+ }
172
+ else {
173
+ const lines = [`${"Column".padEnd(40)} ${"Type".padEnd(20)} ${"Nullable".padEnd(10)} MaxLen`, "-".repeat(80)];
174
+ for (const r of res.recordset) {
175
+ lines.push(`${String(r.COLUMN_NAME).padEnd(40)} ${String(r.DATA_TYPE).padEnd(20)} ${String(r.IS_NULLABLE).padEnd(10)} ${r.CHARACTER_MAXIMUM_LENGTH ?? ""}`);
176
+ }
177
+ result = lines.join("\n");
178
+ }
179
+ }
180
+ else if (name === "run_query") {
181
+ result = await runQuery(String(args?.sql), args?.max_rows);
182
+ }
183
+ else {
184
+ throw new Error(`Unknown tool: ${name}`);
185
+ }
186
+ return { content: [{ type: "text", text: result }] };
187
+ }
188
+ catch (err) {
189
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
190
+ }
191
+ });
192
+ // ── Start ─────────────────────────────────────────────────────────────────────
193
+ const transport = new StdioServerTransport();
194
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@vicinitysoftware/mcp-sql",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Vicinity Software SQL databases",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "vicinity-mcp-sql": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "resources/",
13
+ "README.md"
14
+ ],
15
+ "engines": { "node": ">=18.0.0" },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.0.0",
22
+ "dotenv": "^16.0.0",
23
+ "mssql": "^11.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/mssql": "^9.0.0",
27
+ "@types/node": "^20.0.0",
28
+ "typescript": "^5.0.0"
29
+ }
30
+ }