# Directory Structure ``` ├── LICENSE ├── pom.xml ├── README.md └── src ├── main │ └── java │ └── com │ └── cdata │ └── mcp │ ├── Config.java │ ├── Constants.java │ ├── CsvUtils.java │ ├── CsvWriter.java │ ├── IResource.java │ ├── ITool.java │ ├── JsonSchemaBuilder.java │ ├── Program.java │ ├── resources │ │ └── TableMetadataResource.java │ ├── StringUtil.java │ ├── Table.java │ ├── tools │ │ ├── GetColumnsTool.java │ │ ├── GetTablesTool.java │ │ └── RunQueryTool.java │ └── UrlUtil.java └── test └── java └── com └── cdata └── mcp └── tests ├── JsonSchemaBuilderTests.java └── TableTests.java ``` # Files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # myob-accountright-mcp-server-by-cdata CData's Model Context Protocol (MCP) Server for MYOB AccountRight :heavy_exclamation_mark: This project builds a local, read-only MCP server. For fully remote MCP access, check out the first managed MCP platform: [CData Connect AI](https://www.cdata.com/ai/). For locally hosted, full read, write, update, delete, and action capabilities and a simplified setup, check out our [CData MCP Server for MYOB AccountRight](https://www.cdata.com/drivers/myobaccountright/download/mcp/). ## Purpose We created this read-only MCP Server to allow LLMs (like Claude Desktop) to query live data MYOB AccountRight supported by the [CData JDBC Driver for MYOB AccountRight](https://www.cdata.com/drivers/myobaccountright/jdbc/). CData JDBC Driver connects to MYOB AccountRight by exposing them as relational SQL models. This server wraps that driver and makes MYOB AccountRight data available through a simple MCP interface, so LLMs can retrieve live information by asking natural language questions — no SQL required. ## Setup Guide 1. Clone the repository: ```bash git clone https://github.com/cdatasoftware/myob-accountright-mcp-server-by-cdata.git cd myob-accountright-mcp-server-by-cdata ``` 2. Build the server: ```bash mvn clean install ``` This creates the JAR file: CDataMCP-jar-with-dependencies.jar 2. Download and install the CData JDBC Driver for {source}: [https://www.cdata.com/drivers/myobaccountright/download/jdbc](https://www.cdata.com/drivers/myobaccountright/download/jdbc/) 3. License the CData JDBC Driver: * Navigate to the `lib` folder in the installation directory, typically: * (Windows) `C:\Program Files\CData\CData JDBC Driver for MYOB AccountRight\` * (Mac/Linux) `/Applications/CData JDBC Driver for MYOB AccountRight/` * Run the command `java -jar cdata.jdbc.myobaccountright.jar --license` * Enter your name, email, and "TRIAL" (or your license key). 4. Configure your connection to the data source (Salesforce as an example): * Run the command `java -jar cdata.jdbc.myobaccountright.jar` to open the Connection String utility. <img src="https://github.com/user-attachments/assets/a5b5237b-79c1-472c-8c2f-3f9eb1ac9627" title="CData JDBC Driver Connectiong String utility." width=384px /> * Configure the connection string and click "Test Connection" > **Note:** If the data sources uses OAuth, you will need to authenticate in your browser. * Once successful, copy the connection string for use later. 5. Create a `.prp` file for your JDBC connection (e.g. `myob-accountright.prp`) using the following properties and format: * **Prefix** - a prefix to be used for the tools exposed * **ServerName** - a name for your server * **ServerVersion** - a version for your server * **DriverPath** - the full path to the JAR file for your JDBC driver * **DriverClass** - the name of the JDBC Driver Class (e.g. cdata.jdbc.myobaccountright.MYOBAccountRightDriver) * **JdbcUrl** - the JDBC connection string to use with the CData JDBC Driver to connect to your data (copied from above) * **Tables** - leave blank to access all data, otherwise you can explicitly declare the tables you wish to create access for ```env Prefix=myobaccountright ServerName=CDataMYOBAccountRight ServerVersion=1.0 DriverPath=PATH\TO\cdata.jdbc.myobaccountright.jar DriverClass=cdata.jdbc.myobaccountright.MYOBAccountRightDriver JdbcUrl=jdbc:myobaccountright:InitiateOAuth=GETANDREFRESH; Tables= ``` ## Using the Server with Claude Desktop 1. Create the config file for Claude Desktop ( claude_desktop_config.json) to add the new MCP server, using the format below. If the file already exists, add the entry to the `mcpServers` in the config file. **Windows** ```json { "mcpServers": { "{classname_dash}": { "command": "PATH\\TO\\java.exe", "args": [ "-jar", "PATH\\TO\\CDataMCP-jar-with-dependencies.jar", "PATH\\TO\\myob-accountright.prp" ] }, ... } } ``` **Linux/Mac** ```json { "mcpServers": { "{classname_dash}": { "command": "/PATH/TO/java", "args": [ "-jar", "/PATH/TO/CDataMCP-jar-with-dependencies.jar", "/PATH/TO/myob-accountright.prp" ] }, ... } } ``` If needed, copy the config file to the appropriate directory (Claude Desktop as the example). **Windows** ```bash cp C:\PATH\TO\claude_desktop_config.json %APPDATA%\Claude\claude_desktop_config.json ``` **Linux/Mac** ```bash cp /PATH/TO/claude_desktop_config.json /Users/{user}/Library/Application\ Support/Claude/claude_desktop_config.json' ``` 2. Run or refresh your client (Claude Desktop). > **Note:** You may need to fully exit or quit your Claude Desktop client and re-open it for the MCP Servers to appear. ## Running the Server 1. Run the follow the command to run the MCP Server on its own ```bash java -jar /PATH/TO/CDataMCP-jar-with-dependencies.jar /PATH/TO/Salesforce.prp > **Note:** The server uses `stdio` so can only be used with clients that run on the same machine as the server. ## Usage Details Once the MCP Server is configured, the AI client will be able to use the built-in tools to read, write, update, and delete the underlying data. In general, you do not need to call the tools explicitly. Simply ask the client to answer questions about the underlying data system. For example: * "What is the correlation between my closed won opportunities and the account industry?" * "How many open tickets do I have in the SUPPORT project?" * "Can you tell me what calendar events I have today?" The list of tools available and their descriptions follow: ### Tools & Descriptions In the definitions below, `{servername}` refers to the name of the MCP Server in the config file (e.g. `{classname_dash}` above). * `{servername}_get_tables` - Retrieves a list of tables available in the data source. Use the `{servername}_get_columns` tool to list available columns on a table. The output of the tool will be returned in CSV format, with the first line containing column headers. * `{servername}_get_columns` - Retrieves a list of columns for a table. Use the `{servername}_get_tables` tool to get a list of available tables. The output of the tool will be returned in CSV format, with the first line containing column headers. * `{servername}_run_query` - Execute a SQL SELECT query ## JSON-RPC Request Examples If you are scripting out the requests sent to the MCP Server instead of using an AI Client (e.g. Claude), then you can refer to the JSON payload examples below – following the JSON-RPC 2.0 specification - when calling the available tools. #### source_get_tables ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "source_get_tables", "arguments": {} } } ``` #### source_get_columns ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "source_get_columns", "arguments": { "table": "Account" } } } ``` #### source_run_query ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "source_run_query", "arguments": { "sql": "SELECT * FROM [Account] WHERE [IsDeleted] = true" } } } ``` ## Troubleshooting 1. If you cannot see your CData MCP Server in Claude Desktop, be sure that you have fully quit Claude Desktop (Windows: use the Task Manager, Mac: use the Activity Monitor) 2. If Claude Desktop is unable to retrieve data, be sure that you have configured your connection properly. Use the Connection String builder to create the connection string (see above) and copy the connection string into the property (.prp) file. 3. If you are having trouble connecting to your data source, contact the [CData Support Team](https://www.cdata.com/support/submit.aspx). 4. If you are having trouble using the MCP server, or have any other feedback, join the [CData Community](https://community.cdata.com). ## License This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the [LICENSE](./LICENSE) file in the project repository. ## All Supported Sources <table> <tr><td>Access</td><td>Act CRM</td><td>Act-On</td><td>Active Directory</td></tr> <tr><td>ActiveCampaign</td><td>Acumatica</td><td>Adobe Analytics</td><td>Adobe Commerce</td></tr> <tr><td>ADP</td><td>Airtable</td><td>AlloyDB</td><td>Amazon Athena</td></tr> <tr><td>Amazon DynamoDB</td><td>Amazon Marketplace</td><td>Amazon S3</td><td>Asana</td></tr> <tr><td>Authorize.Net</td><td>Avalara AvaTax</td><td>Avro</td><td>Azure Active Directory</td></tr> <tr><td>Azure Analysis Services</td><td>Azure Data Catalog</td><td>Azure Data Lake Storage</td><td>Azure DevOps</td></tr> <tr><td>Azure Synapse</td><td>Azure Table</td><td>Basecamp</td><td>BigCommerce</td></tr> <tr><td>BigQuery</td><td>Bing Ads</td><td>Bing Search</td><td>Bitbucket</td></tr> <tr><td>Blackbaud FE NXT</td><td>Box</td><td>Bullhorn CRM</td><td>Cassandra</td></tr> <tr><td>Certinia</td><td>Cloudant</td><td>CockroachDB</td><td>Confluence</td></tr> <tr><td>Cosmos DB</td><td>Couchbase</td><td>CouchDB</td><td>CSV</td></tr> <tr><td>Cvent</td><td>Databricks</td><td>DB2</td><td>DocuSign</td></tr> <tr><td>Dropbox</td><td>Dynamics 365</td><td>Dynamics 365 Business Central</td><td>Dynamics CRM</td></tr> <tr><td>Dynamics GP</td><td>Dynamics NAV</td><td>eBay</td><td>eBay Analytics</td></tr> <tr><td>Elasticsearch</td><td>Email</td><td>EnterpriseDB</td><td>Epicor Kinetic</td></tr> <tr><td>Exact Online</td><td>Excel</td><td>Excel Online</td><td>Facebook</td></tr> <tr><td>Facebook Ads</td><td>FHIR</td><td>Freshdesk</td><td>FTP</td></tr> <tr><td>GitHub</td><td>Gmail</td><td>Google Ad Manager</td><td>Google Ads</td></tr> <tr><td>Google Analytics</td><td>Google Calendar</td><td>Google Campaign Manager 360</td><td>Google Cloud Storage</td></tr> <tr><td>Google Contacts</td><td>Google Data Catalog</td><td>Google Directory</td><td>Google Drive</td></tr> <tr><td>Google Search</td><td>Google Sheets</td><td>Google Spanner</td><td>GraphQL</td></tr> <tr><td>Greenhouse</td><td>Greenplum</td><td>HarperDB</td><td>HBase</td></tr> <tr><td>HCL Domino</td><td>HDFS</td><td>Highrise</td><td>Hive</td></tr> <tr><td>HubDB</td><td>HubSpot</td><td>IBM Cloud Data Engine</td><td>IBM Cloud Object Storage</td></tr> <tr><td>IBM Informix</td><td>Impala</td><td>Instagram</td><td>JDBC-ODBC Bridge</td></tr> <tr><td>Jira</td><td>Jira Assets</td><td>Jira Service Management</td><td>JSON</td></tr> <tr><td>Kafka</td><td>Kintone</td><td>LDAP</td><td>LinkedIn</td></tr> <tr><td>LinkedIn Ads</td><td>MailChimp</td><td>MariaDB</td><td>Marketo</td></tr> <tr><td>MarkLogic</td><td>Microsoft Dataverse</td><td>Microsoft Entra ID</td><td>Microsoft Exchange</td></tr> <tr><td>Microsoft OneDrive</td><td>Microsoft Planner</td><td>Microsoft Project</td><td>Microsoft Teams</td></tr> <tr><td>Monday.com</td><td>MongoDB</td><td>MYOB AccountRight</td><td>MySQL</td></tr> <tr><td>nCino</td><td>Neo4J</td><td>NetSuite</td><td>OData</td></tr> <tr><td>Odoo</td><td>Office 365</td><td>Okta</td><td>OneNote</td></tr> <tr><td>Oracle</td><td>Oracle Eloqua</td><td>Oracle Financials Cloud</td><td>Oracle HCM Cloud</td></tr> <tr><td>Oracle Sales</td><td>Oracle SCM</td><td>Oracle Service Cloud</td><td>Outreach.io</td></tr> <tr><td>Parquet</td><td>Paylocity</td><td>PayPal</td><td>Phoenix</td></tr> <tr><td>PingOne</td><td>Pinterest</td><td>Pipedrive</td><td>PostgreSQL</td></tr> <tr><td>Power BI XMLA</td><td>Presto</td><td>Quickbase</td><td>QuickBooks</td></tr> <tr><td>QuickBooks Online</td><td>QuickBooks Time</td><td>Raisers Edge NXT</td><td>Reckon</td></tr> <tr><td>Reckon Accounts Hosted</td><td>Redis</td><td>Redshift</td><td>REST</td></tr> <tr><td>RSS</td><td>Sage 200</td><td>Sage 300</td><td>Sage 50 UK</td></tr> <tr><td>Sage Cloud Accounting</td><td>Sage Intacct</td><td>Salesforce</td><td>Salesforce Data Cloud</td></tr> <tr><td>Salesforce Financial Service Cloud</td><td>Salesforce Marketing</td><td>Salesforce Marketing Cloud Account Engagement</td><td>Salesforce Pardot</td></tr> <tr><td>Salesloft</td><td>SAP</td><td>SAP Ariba Procurement</td><td>SAP Ariba Source</td></tr> <tr><td>SAP Business One</td><td>SAP BusinessObjects BI</td><td>SAP ByDesign</td><td>SAP Concur</td></tr> <tr><td>SAP Fieldglass</td><td>SAP HANA</td><td>SAP HANA XS Advanced</td><td>SAP Hybris C4C</td></tr> <tr><td>SAP Netweaver Gateway</td><td>SAP SuccessFactors</td><td>SAS Data Sets</td><td>SAS xpt</td></tr> <tr><td>SendGrid</td><td>ServiceNow</td><td>SFTP</td><td>SharePoint</td></tr> <tr><td>SharePoint Excel Services</td><td>ShipStation</td><td>Shopify</td><td>SingleStore</td></tr> <tr><td>Slack</td><td>Smartsheet</td><td>Snapchat Ads</td><td>Snowflake</td></tr> <tr><td>Spark</td><td>Splunk</td><td>SQL Analysis Services</td><td>SQL Server</td></tr> <tr><td>Square</td><td>Stripe</td><td>Sugar CRM</td><td>SuiteCRM</td></tr> <tr><td>SurveyMonkey</td><td>Sybase</td><td>Sybase IQ</td><td>Tableau CRM Analytics</td></tr> <tr><td>Tally</td><td>TaxJar</td><td>Teradata</td><td>Tier1</td></tr> <tr><td>TigerGraph</td><td>Trello</td><td>Trino</td><td>Twilio</td></tr> <tr><td>Twitter</td><td>Twitter Ads</td><td>Veeva CRM</td><td>Veeva Vault</td></tr> <tr><td>Wave Financial</td><td>WooCommerce</td><td>WordPress</td><td>Workday</td></tr> <tr><td>xBase</td><td>Xero</td><td>XML</td><td>YouTube Analytics</td></tr> <tr><td>Zendesk</td><td>Zoho Books</td><td>Zoho Creator</td><td>Zoho CRM</td></tr> <tr><td>Zoho Inventory</td><td>Zoho Projects</td><td>Zuora</td><td>... Dozens More</td></tr> </table> ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/Constants.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; public class Constants { public static final String TEXT = "text/plain"; public static final String FORMAT_DESC = "The output of the tool will be returned in CSV format, with the first line containing column headers."; } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/ITool.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; import java.util.Map; public interface ITool { public void register(McpServer.SyncSpec mcp) throws Exception; public McpSchema.CallToolResult run(Map<String, Object> args); } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/IResource.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import java.util.Map; public interface IResource { public void register(McpServer.SyncSpec mcp, Table table); public McpSchema.ReadResourceResult run(McpSchema.ReadResourceRequest args); } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/UrlUtil.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; public class UrlUtil { public static String encode(String part) { return URLEncoder.encode(part, Charset.defaultCharset()); } public static String decode(String part) { return URLDecoder.decode(part, Charset.defaultCharset()); } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/StringUtil.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; public class StringUtil { public static String emptyNull(String t) { if (t != null && t.length() == 0) { return null; } return t; } public static boolean isNullOrEmpty(String s) { return s == null || s.length() == 0; } public static String qualify(Config config, String catalog, String schema, String proc) { StringBuilder result = new StringBuilder(); if (catalog != null && catalog.length() > 0) { result.append(config.quoteIdentifier(catalog)) .append("."); } if (schema != null && schema.length() > 0) { result.append(config.quoteIdentifier(schema)) .append("."); } result.append(config.quoteIdentifier(proc)); return result.toString(); } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/CsvWriter.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; public class CsvWriter { private StringBuilder buffer = new StringBuilder(); public Row row() { return new Row(); } public String end() { return this.buffer.toString(); } public class Row { private StringBuilder row = new StringBuilder(); public Row column(String value) { if (row.length() > 0) { row.append(','); } if (value != null && value.length() > 0) { quote(value); } return this; } public void end() { buffer.append(this.row) .append("\n"); } private void quote(String val) { row.append('"'); for (int i=0; i < val.length(); i++) { char ch = val.charAt(i); if (ch == '"') { row.append(ch); } row.append(ch); } row.append('"'); } } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/CsvUtils.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; public class CsvUtils { public static final String MIME = "text/csv"; public static String resultSetToCsv(ResultSet rs) throws SQLException { return resultSetToCsv(rs, null); } public static String resultSetToCsv(ResultSet rs, String[][] columns) throws SQLException { CsvWriter csv = new CsvWriter(); ResultSetMetaData meta = rs.getMetaData(); writeMeta(csv, meta, columns); while (rs.next()) { writeRow(csv, meta, columns, rs); } rs.close(); return csv.end(); } private static void writeMeta(CsvWriter csv, ResultSetMetaData meta, String[][] columns) throws SQLException { CsvWriter.Row row = csv.row(); if (columns == null) { for (int i = 1; i <= meta.getColumnCount(); i++) { row.column(meta.getColumnLabel(i)); } } else { for ( int i=0; i < columns.length; i++ ) { row.column(columns[i][1]); } } row.end(); } private static void writeRow(CsvWriter csv, ResultSetMetaData meta, String[][] columns, ResultSet rs) throws SQLException { CsvWriter.Row row = csv.row(); if ( columns == null ) { for (int i = 1; i <= meta.getColumnCount(); i++) { row.column(rs.getString(i)); } } else { for (int i = 0; i < columns.length; i++) { row.column(rs.getString(columns[i][0])); } } row.end(); } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/resources/TableMetadataResource.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp.resources; import com.cdata.mcp.*; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import java.sql.Connection; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; public class TableMetadataResource implements IResource { private Config config; private static final String[][] META_COLS = new String[][] { new String[] { "TABLE_CAT", "Catalog" }, new String[] { "TABLE_SCHEM", "Schema" }, new String[] { "TABLE_NAME", "Table" }, new String[] { "COLUMN_NAME", "Column" }, new String[] { "TYPE_NAME", "DataType" } }; public TableMetadataResource(Config config) { this.config = config; } @Override public void register(McpServer.SyncSpec mcp, Table table) { String uri = config.getMcpScheme() + table.urlPath(); McpServerFeatures.SyncResourceRegistration resource = new McpServerFeatures.SyncResourceRegistration( new McpSchema.Resource(uri, table.fullName(), "List of columns for table " + table.fullName(), CsvUtils.MIME, null), this::run ); mcp.resources(resource); } @Override public McpSchema.ReadResourceResult run(McpSchema.ReadResourceRequest args) { try { Table table = Table.fromUri(args.uri()); try (Connection cn = config.newConnection()) { ResultSet rs = cn.getMetaData().getColumns(table.catalog(), table.schema(), table.name(), null); List<McpSchema.ResourceContents> contents = new ArrayList<>(); contents.add( new McpSchema.TextResourceContents(args.uri(), CsvUtils.MIME, CsvUtils.resultSetToCsv(rs, META_COLS)) ); return new McpSchema.ReadResourceResult(contents); } } catch (Exception ex) { throw new RuntimeException("Error: " + ex.getMessage()); } } } ``` -------------------------------------------------------------------------------- /src/test/java/com/cdata/mcp/tests/JsonSchemaBuilderTests.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp.tests; import com.cdata.mcp.JsonSchemaBuilder; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.type.MapType; import com.fasterxml.jackson.databind.type.TypeFactory; import org.junit.Assert; import org.junit.Test; import java.util.HashMap; import java.util.Map; public class JsonSchemaBuilderTests { @Test public void simpleStringProperty() throws Exception { JsonSchemaBuilder builder = new JsonSchemaBuilder(); builder.addString("myprop", "my desc"); String result = builder.build(); Map<String, Object> obj = deserialize(result); Assert.assertEquals(JsonSchemaBuilder.SCHEMA_REF, obj.get("$schema")); Assert.assertEquals(JsonSchemaBuilder.OBJECT, obj.get("type")); Map<String, Object> props = (Map<String, Object>)obj.get("properties"); Map<String, Object> myprop = (Map<String, Object>)props.get("myprop"); Assert.assertEquals(JsonSchemaBuilder.STRING, myprop.get("type")); Assert.assertEquals("my desc", myprop.get("description")); } @Test public void simpleObjectProperty() throws Exception { JsonSchemaBuilder builder = new JsonSchemaBuilder(); builder.addObject("myprop", "my desc"); String result = builder.build(); Map<String, Object> obj = deserialize(result); Assert.assertEquals(JsonSchemaBuilder.SCHEMA_REF, obj.get("$schema")); Assert.assertEquals(JsonSchemaBuilder.OBJECT, obj.get("type")); Map<String, Object> props = (Map<String, Object>)obj.get("properties"); Map<String, Object> myprop = (Map<String, Object>)props.get("myprop"); Assert.assertEquals(JsonSchemaBuilder.OBJECT, myprop.get("type")); Assert.assertEquals("my desc", myprop.get("description")); } private Map<String, Object> deserialize(String result) throws Exception { TypeFactory factory = TypeFactory.defaultInstance(); MapType type = factory.constructMapType(HashMap.class, String.class, Object.class); return new ObjectMapper().readValue(result, type); } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/JsonSchemaBuilder.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; public class JsonSchemaBuilder { public static final String STRING = "string"; public static final String INTEGER = "integer"; public static final String NUMBER = "number"; public static final String BOOLEAN = "boolean"; public static final String OBJECT = "object"; public static final String SCHEMA_REF = "http://json-schema.org/draft-07/schema#"; private List<Property> properties = new ArrayList<>(); private List<String> required = new ArrayList<>(); public JsonSchemaBuilder addString(String name, String description) { this.properties.add(new Property(name, STRING, description)); return this; } public JsonSchemaBuilder addObject(String name, String description) { this.properties.add(new Property(name, OBJECT, description)); return this; } public JsonSchemaBuilder add(String name, String type, String description) { this.properties.add(new Property(name, type, description)); return this; } public JsonSchemaBuilder required(String... args) { for (String s : args ) { this.required.add(s); } return this; } public String build() throws Exception { Map<String, Object> props = new LinkedHashMap<>(); for ( Property p : this.properties ) { Map<String, Object> prop = new LinkedHashMap<>(); prop.put("type", p.type()); if (p.hasDescription()) { prop.put("description", p.description()); } props.put(p.name(), prop); } Map<String, Object> obj = new LinkedHashMap<>(); obj.put("$schema", SCHEMA_REF); obj.put("type", OBJECT); obj.put("properties", props); obj.put("required", this.required); return new ObjectMapper().writeValueAsString(obj); } static class Property { private String _name; private String _type; private String _description; public Property(String name, String type, String desc) { this._name = name; this._type = type; this._description = desc; } public String name() { return this._name; } public String type() { return this._type; } public boolean hasDescription() { return this._description != null && this._description.length() > 0; } public String description() { return this._description; } } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/tools/RunQueryTool.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp.tools; import com.cdata.mcp.*; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.Tool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Map; public class RunQueryTool implements ITool { private Config config; private Logger logger = LoggerFactory.getLogger(RunQueryTool.class); public RunQueryTool(Config config) { this.config = config; } @Override public void register(McpServer.SyncSpec mcp) throws Exception { String quotes = this.config.getIdentifierQuotes(); String prefix = this.config.getPrefix(); String description = "The SELECT statement to execute. " + "Use the `" + prefix + "_get_tables` tool to get a list of available tables, " + "and the `" + prefix + "_get_columns` tool to list table columns. " + "The SQL dialect is mostly based around SQL-92. " + "Identifiers should be quoted using `" + quotes + "` characters. " + "Valid clauses: FROM, INNER JOIN, LEFT JOIN, GROUP BY, ORDER BY, LIMIT/OFFSET. " + Constants.FORMAT_DESC; String schema = new JsonSchemaBuilder() .addString("sql", description) .build(); mcp.tool( new Tool( prefix + "_run_query", "Execute a SQL SELECT statement.", schema ), this::run ); } @Override public McpSchema.CallToolResult run(Map<String, Object> args) { String sql = (String)args.get("sql"); this.logger.info("RunQueryTool({})", sql); try { try (Connection cn = config.newConnection()) { List<McpSchema.Content> content = new ArrayList<>(); String csv = queryToCsv(cn, sql); List<McpSchema.Role> roles = new ArrayList<>(); roles.add(McpSchema.Role.USER); content.add( new McpSchema.TextContent(roles, 1.0, csv) ); return new McpSchema.CallToolResult(content, false); } } catch ( Exception ex ) { throw new RuntimeException("ERROR: " + ex.getMessage()); } } private String queryToCsv(Connection cn, String sql) throws SQLException { try (Statement st = cn.createStatement()) { return CsvUtils.resultSetToCsv(st.executeQuery(sql)); } } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/tools/GetColumnsTool.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp.tools; import com.cdata.mcp.*; import static com.cdata.mcp.StringUtil.*; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.Map; public class GetColumnsTool implements ITool { private Config config; private Logger logger = LoggerFactory.getLogger(GetColumnsTool.class); public GetColumnsTool(Config config) { this.config = config; } public void register(McpServer.SyncSpec mcp) throws Exception { String schema = new JsonSchemaBuilder() .addString("catalog", "The catalog name") .addString("schema", "The schema name") .addString("table", "The table name") .required("table") .build(); mcp.tool( new McpSchema.Tool( config.getPrefix() + "_get_columns", "Retrieves a list of fields, dimensions, or measures (as columns) for an object, entity or collection (table). Use the `" + config.getPrefix() + "_get_tables` tool to get a list of available tables. " + Constants.FORMAT_DESC, schema ), this::run ); } @Override public McpSchema.CallToolResult run(Map<String, Object> args) { String catalog = (String)args.get("catalog"); String schema = (String)args.get("schema"); String table = (String)args.get("table"); this.logger.info("GetColumnsTool({}, {}, {})", catalog, schema, table); try { try (Connection cn = config.newConnection()) { List<McpSchema.Content> content = new ArrayList<>(); String csv = columnsToCsv(cn, catalog, schema, table); List<McpSchema.Role> roles = new ArrayList<>(); roles.add(McpSchema.Role.USER); content.add( new McpSchema.TextContent(roles, 1.0, csv) ); return new McpSchema.CallToolResult(content, false); } } catch ( Exception ex ) { throw new RuntimeException("ERROR: " + ex.getMessage()); } } private String columnsToCsv(Connection cn, String catalog, String schema, String table) throws SQLException { List<String[]> META_COLS = new ArrayList<String[]>(); if (this.config.supportsMultipleCatalogs()) { META_COLS.add(new String[]{"TABLE_CAT", "Catalog"}); } if (this.config.supportsMultipleSchemas()) { META_COLS.add(new String[]{"TABLE_SCHEM", "Schema"}); } META_COLS.add(new String[] { "TABLE_NAME", "Table" }); META_COLS.add(new String[] { "COLUMN_NAME", "Column" }); META_COLS.add(new String[] { "TYPE_NAME", "DataType" }); META_COLS.add(new String[] { "REMARKS", "Remarks" }); DatabaseMetaData meta = cn.getMetaData(); try (ResultSet rs = meta.getColumns(emptyNull(catalog), emptyNull(schema), table, null)) { return CsvUtils.resultSetToCsv(rs, META_COLS.toArray(new String[0][])); } } } ``` -------------------------------------------------------------------------------- /src/test/java/com/cdata/mcp/tests/TableTests.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp.tests; import com.cdata.mcp.Table; import org.junit.Assert; import org.junit.Test; import java.util.List; public class TableTests { @Test public void justName() { Table table = Table.parse("mytable"); Assert.assertEquals("mytable", table.name()); } @Test public void justSchemaName() { Table table = Table.parse("myschema.mytable"); Assert.assertEquals("myschema", table.schema()); Assert.assertEquals("mytable", table.name()); } @Test public void fullName() { Table table = Table.parse("mycatalog.myschema.mytable"); Assert.assertEquals("mycatalog", table.catalog()); Assert.assertEquals("myschema", table.schema()); Assert.assertEquals("mytable", table.name()); } @Test public void quoted() { Table table = Table.parse("[mycatalog].[myschema].[mytable]"); Assert.assertEquals("mycatalog", table.catalog()); Assert.assertEquals("myschema", table.schema()); Assert.assertEquals("mytable", table.name()); } @Test public void spacesAtStartAreIgnored() { Table table = Table.parse(" [mycatalog].[myschema].[mytable]"); Assert.assertEquals("mycatalog", table.catalog()); Assert.assertEquals("myschema", table.schema()); Assert.assertEquals("mytable", table.name()); } @Test public void spacesAtEndAreIgnored() { Table table = Table.parse("[mycatalog].[myschema].mytable "); Assert.assertEquals("mycatalog", table.catalog()); Assert.assertEquals("myschema", table.schema()); Assert.assertEquals("mytable", table.name()); } @Test public void parseList() { String list = "[mycatalog].[myschema].mytable, cat2.schm2.t2, cat3.[scm3].t3 "; List<Table> tables = Table.parseList(list); Assert.assertEquals(3, tables.size()); Table table = tables.get(0); Assert.assertEquals("mycatalog", table.catalog()); Assert.assertEquals("myschema", table.schema()); Assert.assertEquals("mytable", table.name()); table = tables.get(1); Assert.assertEquals("cat2", table.catalog()); Assert.assertEquals("schm2", table.schema()); Assert.assertEquals("t2", table.name()); table = tables.get(2); Assert.assertEquals("cat3", table.catalog()); Assert.assertEquals("scm3", table.schema()); Assert.assertEquals("t3", table.name()); } @Test public void parseList2() { String list = "table1, table2"; List<Table> tables = Table.parseList(list); Assert.assertEquals(2, tables.size()); Table table = tables.get(0); Assert.assertEquals("table1", table.name()); table = tables.get(1); Assert.assertEquals("table2", table.name()); } @Test public void fromUri1() throws Exception { String uri = "salesforce://cat/schm/table"; Table table = Table.fromUri(uri); Assert.assertNotNull(table); Assert.assertEquals("cat", table.catalog()); Assert.assertEquals("schm", table.schema()); Assert.assertEquals("table", table.name()); } } ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cdata</groupId> <artifactId>CDataMCP</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <finalName>CDataMCP</finalName> <archive> <manifest> <addClasspath>true</addClasspath> <mainClass>com.cdata.mcp.Program</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <!-- this is used for inheritance merges --> <phase>package</phase> <!-- bind to the packaging phase --> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> <version>0.8.1</version> </dependency> <!-- <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>12.0.16</version> </dependency> <dependency> <groupId>org.eclipse.jetty.ee10</groupId> <artifactId>jetty-ee10-servlet</artifactId> <version>12.0.16</version> </dependency> <dependency> <groupId>org.eclipse.jetty.ee10</groupId> <artifactId>jetty-ee10-webapp</artifactId> <version>12.0.16</version> </dependency> --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.16</version> </dependency> <!-- <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-nop</artifactId> <version>2.0.16</version> </dependency> --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> </project> ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/Program.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import java.sql.SQLException; import java.util.List; import com.cdata.mcp.resources.TableMetadataResource; import com.cdata.mcp.tools.GetColumnsTool; import com.cdata.mcp.tools.GetTablesTool; import com.cdata.mcp.tools.RunQueryTool; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.StdioServerTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.ServerMcpTransport; public class Program { private ServerMcpTransport transport; private Config config; private McpSyncServer mcpServer; private static final boolean STDIO = true; public void init(String configPath) throws Exception { this.config = new Config(); this.config.load(configPath); if (!StringUtil.isNullOrEmpty(this.config.getLogFile())) { System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug"); System.setProperty("org.slf4j.simpleLogger.logFile", this.config.getLogFile()); } if (!this.config.validate(System.err)) { System.exit(-1); } this.transport = new StdioServerTransport(new ObjectMapper()); } public void configureMcp() throws Exception { McpServer.SyncSpec spec = McpServer.sync(this.transport) .serverInfo(this.config.getServerName(), this.config.getServerVersion()) .capabilities( McpSchema.ServerCapabilities.builder() .tools(true) .resources(false, true) //.logging() .build() ); registerResources(this.config, spec); registerTools(this.config, spec); this.mcpServer = spec.build(); } public static void main(String[] args) throws Exception { if (args.length < 1) { System.err.println("Usage: <properties-file-path>"); System.exit(-1); } String path = args[0]; final Program p = new Program(); p.init(args[0]); p.configureMcp(); if (!STDIO) { //p.runHttpServer(); } else { Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { synchronized (p) { p.notify(); } } }); synchronized (p) { p.wait(); p.mcpServer.closeGracefully(); } } } private static void registerResources(Config config, McpServer.SyncSpec mcp) throws SQLException { List<Table> tables = config.getTables(); TableMetadataResource resource = new TableMetadataResource(config); for (Table r : tables) { resource.register(mcp, r); } } private static void registerTools(Config config, McpServer.SyncSpec mcp) throws Exception { ITool[] tools = new ITool[] { new GetTablesTool(config), new GetColumnsTool(config), new RunQueryTool(config) }; for (ITool tool : tools) { if (tool != null) { tool.register(mcp); } } } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/tools/GetTablesTool.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp.tools; import com.cdata.mcp.*; import static com.cdata.mcp.StringUtil.*; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; public class GetTablesTool implements ITool { private Config config; private Logger logger = LoggerFactory.getLogger(GetTablesTool.class); public GetTablesTool(Config config) { this.config = config; } public void register(McpServer.SyncSpec mcp) throws Exception { String schema = new JsonSchemaBuilder() .addString("catalog", "The catalog name") .addString("schema", "The schema name") .build(); mcp.tool( new McpSchema.Tool( config.getPrefix() + "_get_tables", "Retrieves a list of objects, entities, collections, etc. (as tables) available in the data source. Use the `" + config.getPrefix() + "_get_columns` tool to list available columns on a table. " + "Both `catalog` and `schema` are optional parameters. " + Constants.FORMAT_DESC, schema ), this::run ); } @Override public McpSchema.CallToolResult run(Map<String, Object> args) { String catalog = (String)args.get("catalog"); String schema = (String)args.get("schema"); this.logger.info("GetTablesTool({}, {})", catalog, schema); try { try (Connection cn = config.newConnection()) { List<McpSchema.Content> content = new ArrayList<>(); String csv = tablesToCsv(cn, catalog, schema); List<McpSchema.Role> roles = new ArrayList<>(); roles.add(McpSchema.Role.USER); content.add( new McpSchema.TextContent(roles, 1.0, csv) ); if (!this.config.supportsMultipleCatalogs()) { content.add( new McpSchema.TextContent(roles, 1.0, "Default Catalog: " + this.config.defaultCatalog()) ); } if (!this.config.supportsMultipleSchemas()) { content.add( new McpSchema.TextContent(roles, 1.0, "Default Schema: " + this.config.defaultSchema()) ); } return new McpSchema.CallToolResult(content, false); } } catch ( Exception ex ) { throw new RuntimeException("ERROR: " + ex.getMessage()); } } private String tablesToCsv(Connection cn, String catalog, String schema) throws SQLException { List<String[]> META_COLS = new ArrayList<String[]>(); if (this.config.supportsMultipleCatalogs()) { META_COLS.add(new String[]{"TABLE_CAT", "Catalog"}); } if (this.config.supportsMultipleSchemas()) { META_COLS.add(new String[]{"TABLE_SCHEM", "Schema"}); } META_COLS.add(new String[] { "TABLE_NAME", "Table" }); META_COLS.add(new String[] { "REMARKS", "Description" }); DatabaseMetaData meta = cn.getMetaData(); try (ResultSet rs = meta.getTables(emptyNull(catalog), emptyNull(schema), null, null)) { return CsvUtils.resultSetToCsv(rs, META_COLS.toArray(new String[0][])); } } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/Table.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import java.net.URI; import java.util.ArrayList; import java.util.List; public class Table { private String _catalog; private String _schema; private String _name; private Table() { } public Table(String cat, String sch, String n) { this._catalog = cat; this._schema = sch; this._name = n; } public boolean hasCatalog() { return this._catalog != null && this._catalog.length() > 0; } public String catalog() { return this._catalog; } public boolean hasSchema() { return this._schema != null && this._schema.length() > 0; } public String schema() { return this._schema; } public String name() { return this._name; } public static Table parse(String text) { Tokenizer t = new Tokenizer(text); return parseInt(t); } public static List<Table> parseList(String text) { List<Table> list = new ArrayList<>(); Tokenizer t = new Tokenizer(text); while (!t.eof()) { if (list.size() > 0) { t.skipListDelimiter(); } Table table = parseInt(t); if (table != null) { list.add(table); } } return list; } public static Table fromUri(String uri) throws Exception { URI u = new URI(uri); String path = u.getAuthority() + u.getPath(); String[] parts = path.split("/"); if (parts.length == 3) { return new Table(UrlUtil.decode(parts[0]), UrlUtil.decode(parts[1]), UrlUtil.decode(parts[2])); } else if (parts.length == 2) { return new Table("", UrlUtil.decode(parts[0]), UrlUtil.decode(parts[1])); } else { return new Table("", "", UrlUtil.decode(parts[0])); } } public String urlPath() { StringBuilder path = new StringBuilder(); if (this._catalog != null && this._catalog.length() > 0) { path.append(UrlUtil.encode(this._catalog)); } path.append("/"); if (this._schema != null && this._schema.length() > 0) { path.append(UrlUtil.encode(this._schema)); } path.append("/"); path.append(UrlUtil.encode(this._name)); return path.toString(); } public String fullName() { StringBuilder full = new StringBuilder(); if (this._catalog != null && this._catalog.length() > 0) { full.append('[').append(this._catalog).append(']'); } full.append("."); if (this._schema != null && this._schema.length() > 0) { full.append('[').append(this._schema).append(']'); } full.append("."); full.append('[').append(this._name).append(']'); return full.toString(); } private static Table parseInt(Tokenizer t) { List<String> names = new ArrayList<String>(); do { String token = t.next(); if (".".equals(token)) { names.add(""); token = t.next(); } else { names.add(token); } token = t.lookahead(); if (".".equals(token)) { t.next(); // discard } else { break; } } while (true); if (names.size() == 3) { return new Table(names.get(0), names.get(1), names.get(2)); } else if (names.size() == 2) { return new Table("", names.get(0), names.get(1)); } else if (names.size() == 1) { return new Table("", "", names.get(0)); } throw new RuntimeException("Invalid table name"); } private static class Tokenizer { private String text; private int offset; private int mark; public Tokenizer(String t) { this.text = t; this.offset = -1; } public boolean eof() { return available() <= 0; } public int available() { if (this.offset < 0) { return this.text.length(); } return this.text.length() - this.offset - 1; } public void mark() { this.mark = this.offset; } public void rollback() { this.offset = this.mark; } public char peek() { if (available() > 0) { return this.text.charAt(this.offset+1); } return '\0'; } public char nextChar() { if (available() > 0) { this.offset++; return this.text.charAt(this.offset); } return '\0'; } public String lookahead() { this.mark(); String n = next(); this.rollback(); return n; } public String next() { StringBuilder token = new StringBuilder(); boolean quoted = false; skipWhitespace(); while (!this.eof()) { char p = peek(); if (token.length() == 0) { if (p == '[') { quoted = true; nextChar(); // consume continue; } token.append(p); nextChar(); // consume } if (!quoted && !isValidChar(p)) { skipWhitespace(); break; } else if (quoted && p == ']'){ quoted = false; nextChar(); // consume ] break; } else { token.append(nextChar()); } } return token.toString(); } private boolean isValidChar(char ch) { return Character.isLetter(ch) || Character.isDigit(ch); } private void skipWhitespace() { while (!eof()) { char p = peek(); if (p != ' ') { break; } nextChar(); } } private void skipToDot() { while (!eof()) { char p = peek(); if (p == '.') { break; } nextChar(); } } private void skipListDelimiter() { while (!eof()) { char p = nextChar(); if (p == ',') { break; } } } } } ``` -------------------------------------------------------------------------------- /src/main/java/com/cdata/mcp/Config.java: -------------------------------------------------------------------------------- ```java package com.cdata.mcp; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintStream; import java.net.URL; import java.net.URLClassLoader; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.Driver; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Properties; import static com.cdata.mcp.StringUtil.isNullOrEmpty; public class Config { private static final String PREFIX = "Prefix"; private static final String DRIVER = "DriverClass"; private static final String DRIVER_JAR = "DriverPath"; private static final String JDBC_URL = "JdbcUrl"; private static final String TABLES = "Tables"; private static final String LOG_FILE = "LogFile"; // following properties will be discovered dynamically from driver private static final String ID_QUOTE_OPEN_CHAR = "IDENTIFIER_QUOTE_OPEN_CHAR"; private static final String ID_QUOTE_CLOSE_CHAR = "IDENTIFIER_QUOTE_CLOSE_CHAR"; private static final String SUPPORTS_MULTIPLE_CATALOGS = "SUPPORTS_MULTIPLE_CATALOGS"; private static final String SUPPORTS_MULTIPLE_SCHEMAS = "SUPPORTS_MULTIPLE_SCHEMAS"; private Properties props = new Properties(); private Properties sqlInfo = new Properties(); private Driver driver; private String defCatalog; private String defSchema; public void load(String filepath) throws IOException { try (FileInputStream fis = new FileInputStream(filepath)) { props.load(fis); } } public boolean validate(PrintStream errors) { boolean result = true; if (isNullOrEmpty(getPrefix())) { errors.println("The '" + PREFIX + "' option is missing"); result = false; } if (isNullOrEmpty(getDriver())) { errors.println("The '" + DRIVER + "' option is missing"); result = false; } if (isNullOrEmpty(getDriverJar())) { errors.println("The '" + DRIVER_JAR + "' option is missing"); result = false; } else if (!verifyDriverLoad(errors)) { result = false; } if (isNullOrEmpty(getJdbcUrl())) { errors.println("The '" + JDBC_URL + "' option is missing"); result = false; } else if (result && !verifyJdbcUrl(errors)) { result = false; } return result; } public String getServerName() { return this.getPrefix(); } public String getServerVersion() { return "1.0"; } public String getPrefix() { return this.props.getProperty(PREFIX); } public String getMcpScheme() { return getPrefix() + "://"; } public String getDriver() { return this.props.getProperty(DRIVER); } public String getDriverJar() { return this.props.getProperty(DRIVER_JAR); } public String getJdbcUrl() { return this.props.getProperty(JDBC_URL); } public List<Table> getTables() throws SQLException { String tables = this.props.getProperty(TABLES); if (isNullOrEmpty(tables)) { return new ArrayList<>(); } List<Table> entries = Table.parseList(tables); return completeTableList(entries); } public String getIdentifierQuotes() { return this.sqlInfo.getProperty(ID_QUOTE_OPEN_CHAR) + this.sqlInfo.getProperty(ID_QUOTE_CLOSE_CHAR); } public boolean supportsMultipleCatalogs() { String val = this.sqlInfo.getProperty(SUPPORTS_MULTIPLE_CATALOGS); return val.equalsIgnoreCase("YES"); } public boolean supportsMultipleSchemas() { String val = this.sqlInfo.getProperty(SUPPORTS_MULTIPLE_SCHEMAS); return val.equalsIgnoreCase("YES"); } public String defaultCatalog() { return this.defCatalog; } public String defaultSchema() { return this.defSchema; } public String getLogFile() { return this.props.getProperty(LOG_FILE); } public String quoteIdentifier(String id) { String open = this.sqlInfo.getProperty(ID_QUOTE_OPEN_CHAR); String close = this.sqlInfo.getProperty(ID_QUOTE_CLOSE_CHAR); // TODO: Properly escape things return open + id + close; } public Connection newConnection() throws SQLException { return this.driver.connect(this.getJdbcUrl(), new Properties()); } private boolean verifyDriverLoad(PrintStream errors) { if (!new File(getDriverJar()).exists()) { errors.println("The '" + DRIVER_JAR + "' option is not a valid JAR file"); return false; } try { loadDriver(); return true; } catch (Throwable t) { String msg = t.getClass().getName() + ": " + t.getMessage(); errors.println("Attempting to load the JDBC driver failed: " + msg); } return false; } private boolean verifyJdbcUrl(PrintStream errors) { try { try (Connection cn = newConnection()) { } return true; } catch ( SQLException ex ) { errors.println("Failed to open JDBC connection: " + ex.getMessage()); } return false; } private void loadDriver() throws Exception { URLClassLoader ucl = new URLClassLoader( new URL[] { new File(this.getDriverJar()).toURI().toURL(), }, this.getClass().getClassLoader() ); Class dc = ucl.loadClass(this.getDriver()); this.driver = (Driver)dc.getDeclaredConstructor().newInstance(); loadSqlInfo(); } private void loadSqlInfo() throws SQLException { try (Connection cn = newConnection()) { retrieveSqlInfo(cn); if (!this.supportsMultipleCatalogs()) { if (!this.supportsMultipleSchemas()) { retrieveDefaultCatalogAndSchema(cn); } else { retrieveDefaultCatalog(cn); } } } } private void retrieveDefaultCatalogAndSchema(Connection cn) throws SQLException { DatabaseMetaData meta = cn.getMetaData(); try (ResultSet rs = meta.getSchemas()) { rs.next(); this.defCatalog = rs.getString("TABLE_CATALOG"); this.defSchema = rs.getString("TABLE_SCHEM"); } } private void retrieveDefaultCatalog(Connection cn) throws SQLException { DatabaseMetaData meta = cn.getMetaData(); try (ResultSet rs = meta.getCatalogs()) { rs.next(); this.defCatalog = rs.getString(1); } } private void retrieveSqlInfo(Connection cn) throws SQLException { try (Statement st = cn.createStatement()) { try (ResultSet rs = st.executeQuery("SELECT NAME, VALUE FROM sys_sqlinfo")) { while (rs.next()) { String key = rs.getString(1); String value = rs.getString(2); if (value == null) { value = ""; } this.sqlInfo.put(key, value); } } } } private List<Table> completeTableList(List<Table> list) throws SQLException { List<Table> result = new ArrayList<>(); try (Connection cn = newConnection()) { DatabaseMetaData meta = cn.getMetaData(); // Not the most efficient, but oh well for (Table t : list) { addMatchingTables(t, meta, result); } } return result; } private void addMatchingTables(Table t, DatabaseMetaData meta, List<Table> result) throws SQLException { String catalog = t.hasCatalog() ? t.catalog() : null; String schema = t.hasSchema() ? t.schema() : null; String name = t.name(); try (ResultSet rs = meta.getTables(catalog, schema, name, null)) { while (rs.next()) { catalog = rs.getString("TABLE_CAT"); schema = rs.getString("TABLE_SCHEM"); name = rs.getString("TABLE_NAME"); result.add(new Table(catalog, schema, name)); } } } } ```