Skip to content

Commit c54eed0

Browse files
author
Daniele Briggi
committed
fix(module): declare exported classes
1 parent cb1782d commit c54eed0

15 files changed

+487
-55
lines changed

dist/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { PortableSSEServerTransport } from './portableSseTransport.js';
2+
export { SQLiteCloudMcpTransport } from './sqlitecloudTransport.js';
3+
export { SQLiteCloudMcpServer } from './server.js';

dist/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { PortableSSEServerTransport } from './portableSseTransport.js';
2+
export { SQLiteCloudMcpTransport } from './sqlitecloudTransport.js';
3+
export { SQLiteCloudMcpServer } from './server.js';

dist/main.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
export {};

dist/main.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env node
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3+
import { SQLiteCloudMcpServer } from './server.js';
4+
import { SQLiteCloudMcpTransport } from './sqlitecloudTransport.js';
5+
import { parseArgs } from 'util';
6+
const server = new SQLiteCloudMcpServer();
7+
async function main() {
8+
// console.debug('Starting SQLite Cloud MCP Server...')
9+
const { values: { connectionString } } = parseArgs({
10+
options: {
11+
connectionString: {
12+
type: 'string'
13+
}
14+
}
15+
});
16+
if (!connectionString) {
17+
throw new Error('Please provide a Connection String with the --connectionString flag');
18+
}
19+
const transport = new SQLiteCloudMcpTransport(connectionString, new StdioServerTransport());
20+
await server.connect(transport);
21+
// console.debug('SQLite Cloud MCP Server running on stdio')
22+
}
23+
main().catch(error => {
24+
console.error('Fatal error in main():', error);
25+
process.exit(1);
26+
});

dist/portableSseTransport.d.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
2+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
3+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
4+
interface PortableWriter {
5+
write: (message: string) => Promise<void>;
6+
close: () => void;
7+
}
8+
/**
9+
* Server transport for SSE to send messages over an SSE connection.
10+
*
11+
* This is a reimplementation of the `SSEServerTransport` class from `@modelcontextprotocol/sdk/server/see`
12+
* without the dependency with ExpressJS.
13+
*/
14+
export declare class PortableSSEServerTransport implements Transport {
15+
private _endpoint;
16+
private writableStream;
17+
private _sseWriter?;
18+
private _sessionId;
19+
onclose?: () => void;
20+
onerror?: (error: Error) => void;
21+
onmessage?: (message: JSONRPCMessage, extra?: {
22+
authInfo?: AuthInfo;
23+
}) => void;
24+
/**
25+
* Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
26+
*/
27+
constructor(_endpoint: string, writableStream: PortableWriter);
28+
/**
29+
* Handles the initial SSE connection request.
30+
*
31+
* This should be called when a GET request is made to establish the SSE stream.
32+
*/
33+
start(): Promise<void>;
34+
/**
35+
* Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST.
36+
*/
37+
handleMessage(message: unknown, extra?: {
38+
authInfo?: AuthInfo;
39+
}): Promise<void>;
40+
close(): Promise<void>;
41+
send(message: JSONRPCMessage): Promise<void>;
42+
/**
43+
* Returns the session ID for this transport.
44+
*
45+
* This can be used to route incoming POST requests.
46+
*/
47+
get sessionId(): string;
48+
}
49+
export {};

dist/portableSseTransport.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { randomUUID } from 'node:crypto';
2+
import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
3+
/**
4+
* Server transport for SSE to send messages over an SSE connection.
5+
*
6+
* This is a reimplementation of the `SSEServerTransport` class from `@modelcontextprotocol/sdk/server/see`
7+
* without the dependency with ExpressJS.
8+
*/
9+
export class PortableSSEServerTransport {
10+
_endpoint;
11+
writableStream;
12+
_sseWriter;
13+
_sessionId;
14+
onclose;
15+
onerror;
16+
onmessage;
17+
/**
18+
* Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`.
19+
*/
20+
constructor(_endpoint, writableStream) {
21+
this._endpoint = _endpoint;
22+
this.writableStream = writableStream;
23+
this._sessionId = randomUUID();
24+
}
25+
/**
26+
* Handles the initial SSE connection request.
27+
*
28+
* This should be called when a GET request is made to establish the SSE stream.
29+
*/
30+
async start() {
31+
if (this._sseWriter) {
32+
throw new Error('SSEServerTransport already started! If using Server class, note that connect() calls start() automatically.');
33+
}
34+
this._sseWriter = this.writableStream;
35+
// Send the endpoint event
36+
// Use a dummy base URL because this._endpoint is relative.
37+
// This allows using URL/URLSearchParams for robust parameter handling.
38+
const dummyBase = 'http://localhost'; // Any valid base works
39+
const endpointUrl = new URL(this._endpoint, dummyBase);
40+
endpointUrl.searchParams.set('sessionId', this._sessionId);
41+
// Reconstruct the relative URL string (pathname + search + hash)
42+
const relativeUrlWithSession = endpointUrl.pathname + endpointUrl.search + endpointUrl.hash;
43+
this._sseWriter?.write(`event: endpoint\ndata: ${relativeUrlWithSession}\n\n`);
44+
}
45+
/**
46+
* Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST.
47+
*/
48+
async handleMessage(message, extra) {
49+
if (!this._sseWriter) {
50+
const message = 'SSE connection not established';
51+
throw new Error(message);
52+
}
53+
let parsedMessage;
54+
try {
55+
parsedMessage = JSONRPCMessageSchema.parse(message);
56+
this.onmessage?.(parsedMessage, extra);
57+
}
58+
catch (error) {
59+
this.onerror?.(error);
60+
throw error;
61+
}
62+
}
63+
async close() {
64+
this._sseWriter?.close();
65+
this._sseWriter = undefined;
66+
this.writableStream.close();
67+
this.onclose?.();
68+
}
69+
async send(message) {
70+
if (!this._sseWriter) {
71+
throw new Error('Not connected');
72+
}
73+
this._sseWriter.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
74+
}
75+
/**
76+
* Returns the session ID for this transport.
77+
*
78+
* This can be used to route incoming POST requests.
79+
*/
80+
get sessionId() {
81+
return this._sessionId;
82+
}
83+
}

dist/server.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { z } from 'zod';
2+
import { SQLiteCloudMcpTransport } from './sqlitecloudTransport.js';
3+
export declare class SQLiteCloudMcpServer {
4+
private mcpServer;
5+
private registry;
6+
constructor();
7+
connect(transport: SQLiteCloudMcpTransport): Promise<void>;
8+
getTransport(sessionId: string): SQLiteCloudMcpTransport;
9+
addCustomTool(name: string, description: string, parameters: z.ZodRawShape, handler: (parameters: any, transport: SQLiteCloudMcpTransport) => Promise<any>): void;
10+
removeCustomTool(name: string): void;
11+
private initializeServer;
12+
private setupServer;
13+
}

dist/server.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { z } from 'zod';
3+
export class SQLiteCloudMcpServer {
4+
mcpServer;
5+
registry;
6+
constructor() {
7+
this.registry = {};
8+
this.mcpServer = this.initializeServer();
9+
this.setupServer();
10+
}
11+
async connect(transport) {
12+
const mcpTransport = transport.mcpTransport;
13+
let sessionId = mcpTransport.sessionId;
14+
if (!sessionId) {
15+
sessionId = 'anonymous';
16+
mcpTransport.sessionId = sessionId;
17+
}
18+
mcpTransport.onerror = error => {
19+
console.error('Error in transport:', error);
20+
delete this.registry[sessionId];
21+
};
22+
mcpTransport.onclose = () => {
23+
delete this.registry[sessionId];
24+
};
25+
this.registry[sessionId] = transport;
26+
await this.mcpServer.connect(mcpTransport);
27+
}
28+
getTransport(sessionId) {
29+
const transport = this.registry[sessionId];
30+
if (!transport) {
31+
throw new Error(`Transport not found for session ID: ${sessionId}`);
32+
}
33+
return transport;
34+
}
35+
addCustomTool(name, description, parameters, handler) {
36+
// TODO: keep a registered list of tools to check existence and to implement removal
37+
this.mcpServer.tool(name, description, parameters, async (parameters, extra) => {
38+
if (!extra.sessionId) {
39+
throw new Error('Session ID is required');
40+
}
41+
const customerResult = await handler(parameters, this.getTransport(extra.sessionId));
42+
return { content: [{ type: 'text', text: JSON.stringify(customerResult) }] };
43+
});
44+
}
45+
removeCustomTool(name) {
46+
throw new Error('Not implemented');
47+
}
48+
initializeServer() {
49+
return new McpServer({
50+
name: 'sqlitecloud-mcp-server',
51+
version: '0.0.1',
52+
description: 'MCP Server for SQLite Cloud: https://sqlitecloud.io'
53+
}, {
54+
capabilities: { tools: {} },
55+
instructions: 'This server provides tools to interact with SQLite databases on SQLite Cloud, execute SQL queries, manage table schemas and analyze performance metrics.'
56+
});
57+
}
58+
setupServer() {
59+
this.mcpServer.tool('read-query', 'Execute a SELECT query on the SQLite database on SQLite Cloud', {
60+
query: z.string().describe('SELECT SQL query to execute')
61+
}, async ({ query }, extra) => {
62+
if (!query.trim().toUpperCase().startsWith('SELECT')) {
63+
throw new Error('Only SELECT queries are allowed for read-query');
64+
}
65+
if (!extra.sessionId) {
66+
throw new Error('Session ID is required');
67+
}
68+
const results = await this.getTransport(extra.sessionId).executeQuery(query);
69+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
70+
});
71+
this.mcpServer.tool('write-query', 'Execute a INSERT, UPDATE, or DELETE query on the SQLite database on SQLite Cloud', {
72+
query: z.string().describe('SELECT SQL query to execute')
73+
}, async ({ query }, extra) => {
74+
if (query.trim().toUpperCase().startsWith('SELECT')) {
75+
throw new Error('SELECT queries are not allowed for write_query');
76+
}
77+
if (!extra.sessionId) {
78+
throw new Error('Session ID is required');
79+
}
80+
const results = await this.getTransport(extra.sessionId).executeQuery(query);
81+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
82+
});
83+
this.mcpServer.tool('create-table', 'Create a new table in the SQLite database on SQLite Cloud', {
84+
query: z.string().describe('CREATE TABLE SQL statement')
85+
}, async ({ query }, extra) => {
86+
if (!query.trim().toUpperCase().startsWith('CREATE TABLE')) {
87+
throw new Error('Only CREATE TABLE statements are allowed');
88+
}
89+
if (!extra.sessionId) {
90+
throw new Error('Session ID is required');
91+
}
92+
const results = await this.getTransport(extra.sessionId).executeQuery(query);
93+
return {
94+
content: [{ type: 'text', text: 'Table created successfully' }]
95+
};
96+
});
97+
this.mcpServer.tool('list-tables', 'List all tables in the SQLite database on SQLite Cloud', {}, async ({}, extra) => {
98+
if (!extra.sessionId) {
99+
throw new Error('Session ID is required');
100+
}
101+
const results = await this.getTransport(extra.sessionId).executeQuery("SELECT name FROM sqlite_master WHERE type='table'");
102+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
103+
});
104+
this.mcpServer.tool('describe-table', 'Get the schema information for a specific table on SQLite Cloud database', {
105+
tableName: z.string().describe('Name of the table to describe')
106+
}, async ({ tableName }, extra) => {
107+
if (!extra.sessionId) {
108+
throw new Error('Session ID is required');
109+
}
110+
const results = await this.getTransport(extra.sessionId).executeQuery(`PRAGMA table_info(${tableName})`);
111+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
112+
});
113+
this.mcpServer.tool('list-commands', 'List all available commands and their descriptions from the SQLite database and an external documentation page.', {}, async ({}, extra) => {
114+
try {
115+
if (!extra.sessionId) {
116+
throw new Error('Session ID is required');
117+
}
118+
const results = await this.getTransport(extra.sessionId).executeQuery('LIST COMMANDS;');
119+
// Download the documentation page
120+
const documentationUrl = 'https://raw.githubusercontent.com/sqlitecloud/docs/refs/heads/main/sqlite-cloud/reference/general-commands.mdx';
121+
const response = await fetch(documentationUrl, {
122+
redirect: 'follow'
123+
});
124+
const documentationContent = await response.text();
125+
return {
126+
content: [
127+
{ type: 'text', text: JSON.stringify(results) },
128+
{ type: 'text', text: documentationContent }
129+
]
130+
};
131+
}
132+
catch (error) {
133+
throw new Error('Failed to list commands and fetch documentation.', { cause: error });
134+
}
135+
});
136+
this.mcpServer.tool('execute-command', 'Execute only SQLite Cloud commands listed in the `list-commands` tool. You can use the `list-commands` tool to see the available commands.', {
137+
command: z.string().describe('SQLite Cloud available command to execute')
138+
}, async ({ command }, extra) => {
139+
if (!extra.sessionId) {
140+
throw new Error('Session ID is required');
141+
}
142+
const results = await this.getTransport(extra.sessionId).executeQuery(command);
143+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
144+
});
145+
this.mcpServer.tool('list-analyzer', 'Returns a rowset with the slowest queries performed on the connected this.mcpServer. Supports filtering with GROUPID, DATABASE, GROUPED, and NODE options.', {
146+
groupId: z.string().optional().describe('Group ID to filter the results'),
147+
database: z.string().optional().describe('Database name to filter the results'),
148+
grouped: z.boolean().optional().describe('Whether to group the slowest queries'),
149+
node: z.string().optional().describe('Node ID to execute the command on a specific cluster node')
150+
}, async ({ groupId, database, grouped, node }, extra) => {
151+
let query = 'LIST ANALYZER';
152+
if (groupId)
153+
query += ` GROUPID ${groupId}`;
154+
if (database)
155+
query += ` DATABASE ${database}`;
156+
if (grouped)
157+
query += ' GROUPED';
158+
if (node)
159+
query += ` NODE ${node}`;
160+
if (!extra.sessionId) {
161+
throw new Error('Session ID is required');
162+
}
163+
const results = await this.getTransport(extra.sessionId).executeQuery(query);
164+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
165+
});
166+
this.mcpServer.tool('analyzer-plan-id', 'Gathers information about the indexes used in the query plan of a query execution.', {
167+
queryId: z.string().describe('Query ID to analyze'),
168+
node: z.string().optional().describe('SQLite Cloud Node ID to execute the command on a specific cluster node')
169+
}, async ({ queryId, node }, extra) => {
170+
let query = `ANALYZER PLAN ID ${queryId}`;
171+
if (node)
172+
query += ` NODE ${node}`;
173+
if (!extra.sessionId) {
174+
throw new Error('Session ID is required');
175+
}
176+
const results = await this.getTransport(extra.sessionId).executeQuery(query);
177+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
178+
});
179+
this.mcpServer.tool('analyzer-reset', 'Resets the statistics about a specific query, group of queries, or database.', {
180+
queryId: z.string().optional().describe('Query ID to reset'),
181+
groupId: z.string().optional().describe('Group ID to reset'),
182+
database: z.string().optional().describe('Database name to reset'),
183+
all: z.boolean().optional().describe('Whether to reset all statistics'),
184+
node: z.string().optional().describe('SQLite Cloud Node ID to execute the command on a specific cluster node')
185+
}, async ({ queryId, groupId, database, all, node }, extra) => {
186+
let query = 'ANALYZER RESET';
187+
if (queryId)
188+
query += ` ID ${queryId}`;
189+
if (groupId)
190+
query += ` GROUPID ${groupId}`;
191+
if (database)
192+
query += ` DATABASE ${database}`;
193+
if (all)
194+
query += ' ALL';
195+
if (node)
196+
query += ` NODE ${node}`;
197+
if (!extra.sessionId) {
198+
throw new Error('Session ID is required');
199+
}
200+
const results = await this.getTransport(extra.sessionId).executeQuery(query);
201+
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
202+
});
203+
}
204+
}

dist/sqlitecloudTransport.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2+
export declare class SQLiteCloudMcpTransport {
3+
private connectionString;
4+
mcpTransport: Transport;
5+
constructor(connectionString: string, mcpTransport: Transport);
6+
private getDatabase;
7+
executeQuery(query: string): Promise<any>;
8+
}

0 commit comments

Comments
 (0)