ormcp-server 0.6.1__py3-none-any.whl

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.
README.md ADDED
@@ -0,0 +1,177 @@
1
+ # ORMCP Server
2
+
3
+ **Make your relational data AI ready**
4
+
5
+ ORMCP Server is a Model Context Protocol (MCP) server that enables AI assistants to interact with relational databases using intuitive, object-oriented operations in JSON format.
6
+
7
+ ## What is ORMCP Server?
8
+
9
+ ORMCP Server bridges the gap between AI applications and relational databases. It leverages the Gilhari microservice framework and JDX ORM to provide seamless database access through the standardized MCP protocol.
10
+
11
+ Instead of writing SQL, AI assistants like Claude and Gemini can work with your data using natural language and high-level object operations.
12
+
13
+ ## Key Features
14
+
15
+ - **๐ŸŒ Database Agnostic** - Works with any JDBC-compliant database (PostgreSQL, MySQL, Oracle, SQL Server, SQLite, etc.)
16
+ - **โœ… MCP Compliant** - Fully compatible with the Model Context Protocol standard
17
+ - **๐Ÿ”„ Object-Relational Mapping** - JSON object operations transparently mapped to relational data
18
+ - **๐Ÿงพ Declarative ORM** - Define mappings using simple JDX grammar
19
+ - **๐Ÿš€ High Performance** - Optimized with connection pooling, caching, and minimal database trips
20
+ - **๐Ÿ”’ Secure** - Domain model-specific operations with optional read-only mode
21
+
22
+ ## Quick Start
23
+
24
+ ### Installation
25
+
26
+ ```bash
27
+ pip install ormcp-server
28
+ ```
29
+
30
+ **Existing Gemfury beta users:** If you previously installed via Gemfury, you can continue using that channel or switch to PyPI. Both are supported during the beta period.
31
+
32
+ ### Accessing Full Package with SDK and Examples
33
+
34
+ The source distribution includes everything you need to get started beyond the
35
+ installable package itself โ€” a curated subset of the Gilhari SDK and a
36
+ ready-to-use example microservice.
37
+
38
+ ```bash
39
+ # Download source distribution from PyPI
40
+ pip download --no-binary :all: ormcp-server
41
+
42
+ # Extract it (use the appropriate version number)
43
+ tar -xzf ormcp_server-*.tar.gz
44
+ cd ormcp_server-*/
45
+
46
+ # Now you have access to:
47
+ # - Gilhari_SDK/ Gilhari SDK essentials (libs, config, docs)
48
+ # Note: JDX User Manual excluded due to size; available at
49
+ # https://www.softwaretree.com/v1/products/jdx/docs/JDX-5.0-Manual-v1.pdf
50
+ # - gilhari_example1/ Ready-to-use SQLite-backed example microservice
51
+ # - package/client/ Example MCP client code
52
+ # - package/docs/ Additional ORMCP documentation
53
+ ```
54
+
55
+ **Existing Gemfury beta users:**
56
+ ```bash
57
+ pip download --no-binary :all: \
58
+ --index-url https://YOUR_TOKEN@pypi.fury.io/softwaretree/ \
59
+ --extra-index-url https://pypi.org/simple \
60
+ ormcp-server
61
+ ```
62
+
63
+ ### Basic Usage
64
+
65
+ 1. **Configure and start your Gilhari microservice** (handles database connectivity)
66
+ 2. **Configure ORMCP Server:**
67
+
68
+ ```bash
69
+ export GILHARI_BASE_URL="http://localhost:80/gilhari/v1/"
70
+ export MCP_SERVER_NAME="MyORMCPServer"
71
+ ```
72
+
73
+ 3. **Start the server:**
74
+
75
+ ```bash
76
+ ormcp-server
77
+ ```
78
+
79
+ 4. **Connect your AI client** (e.g., Claude Desktop):
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "my-ormcp-server": {
85
+ "command": "ormcp-server",
86
+ "env": {
87
+ "GILHARI_BASE_URL": "http://localhost:80/gilhari/v1/",
88
+ "MCP_SERVER_NAME": "MyORMCPServer"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## How It Works
96
+
97
+ ```
98
+ AI Assistant <--MCP--> ORMCP Server <--REST--> Gilhari Microservice <--JDBC--> Database
99
+ ```
100
+
101
+ 1. User sends natural language request to AI assistant (e.g., Claude)
102
+ 2. AI assistant translates natural language into MCP tool calls
103
+ 3. ORMCP Server receives MCP tool calls and translates to REST API calls
104
+ 4. Gilhari microservice executes database operations via JDBC/SQL
105
+ 5. Results returned as JSON objects through the chain back to user
106
+
107
+ ## Example Interactions
108
+
109
+ **Query data:**
110
+ ```
111
+ "Show me all users in California"
112
+ "What's the average age of users?"
113
+ ```
114
+
115
+ **Manage data:**
116
+ ```
117
+ "Create a new user named John Smith in Boston"
118
+ "Update all users in New York to have status=active"
119
+ ```
120
+
121
+ **Explore schema:**
122
+ ```
123
+ "What types of objects are available?"
124
+ "Show me the attributes of the User class"
125
+ ```
126
+
127
+ ## Available MCP Tools
128
+
129
+ - **getObjectModelSummary** - Discover classes and relationships
130
+ - **query** - Retrieve objects with filtering and sorting
131
+ - **getObjectById** - Fetch specific objects by primary key
132
+ - **access** - Navigate object relationships
133
+ - **getAggregate** - Calculate COUNT, SUM, AVG, MIN, MAX
134
+ - **insert** - Create new objects
135
+ - **update** - Update existing objects
136
+ - **update2** - Bulk-update existing objects
137
+ - **delete** - Delete objects
138
+ - **delete2** - Bulk-delete existing objects
139
+
140
+ In the READONLY mode, the insert/update/update2/delete/delete2 tools are not available.
141
+
142
+ ## Requirements
143
+
144
+ - Python 3.12+
145
+ - Docker (for Gilhari microservice)
146
+ - JDBC driver for your database
147
+ - MCP-compatible client (Claude Desktop, Gemini CLI, etc.)
148
+
149
+ ## Documentation
150
+
151
+ - **Complete Documentation**: [github.com/softwaretree/ormcp-docs](https://github.com/softwaretree/ormcp-docs)
152
+ - **API Reference**: [ORMCP Tools Reference](https://github.com/softwaretree/ormcp-docs/blob/main/reference/ormcp_tools_reference.md)
153
+ - **Quick Start**: [Getting Started Guide](https://github.com/softwaretree/ormcp-docs#quick-start)
154
+ - **GitHub repositories with working examples**: [Browse Examples](https://github.com/softwaretree/ormcp-docs/tree/main/examples)
155
+ - **Example Microservice**: [github.com/SoftwareTree/gilhari_example1](https://github.com/SoftwareTree/gilhari_example1)
156
+
157
+ ## Beta Notice
158
+
159
+ ORMCP Server is currently in Beta for testing and evaluation purposes only. Not intended for commercial use.
160
+
161
+ ## Support
162
+
163
+ - **Email**: ormcp_support@softwaretree.com
164
+ - **Issues**: [github.com/softwaretree/ormcp-docs/issues](https://github.com/softwaretree/ormcp-docs/issues)
165
+ - **Website**: [softwaretree.com](https://www.softwaretree.com)
166
+
167
+ ## License
168
+
169
+ Proprietary software owned by Software Tree, LLC. See LICENSE for terms.
170
+
171
+ **Beta Evaluation**: Free for testing during evaluation period.
172
+
173
+ **Commercial licensing** terms will be announced at the time of commercial release. For information or to express interest, contact Software Tree at [ormcp_support@softwaretree.com](mailto:ormcp_support@softwaretree.com) or visit [https://www.softwaretree.com](https://www.softwaretree.com).
174
+
175
+ ---
176
+
177
+ **Software Tree** | Making databases AI-accessible
__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ ORMCP Server package
3
+ """
4
+
5
+ __version__ = "0.6.1"
main.py ADDED
@@ -0,0 +1,63 @@
1
+ # Copyright (c) 2025, Software Tree
2
+
3
+ # Alternate entry point for running the ORMCP server in dev/test modes.
4
+
5
+ # Author: Damodar Periwal
6
+ """
7
+ Alternate entry point for running the ORMCP server in dev/test/http modes.
8
+ Example: python main.py --mode http --port 8080
9
+ """
10
+ # main.py
11
+
12
+ # !/usr/bin/env python3
13
+ import argparse
14
+ import sys
15
+ from ormcp_server import run_server, ORMCP_SERVER_VERSION
16
+
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description="ORMCP Dev/Test Launcher")
20
+ parser.add_argument("--version", action="version",
21
+ version=f"ORMCP Server v{ORMCP_SERVER_VERSION}")
22
+
23
+ parser.add_argument(
24
+ "--mode",
25
+ choices=["dev", "test", "stdio", "http"],
26
+ default="dev",
27
+ help="Run mode: dev = stdio, test = mock, stdio = explicit stdio mode, http = HTTP server"
28
+ )
29
+ parser.add_argument(
30
+ "--host", default="127.0.0.1",
31
+ help="Host to bind for HTTP mode (ignored for stdio)"
32
+ )
33
+ parser.add_argument(
34
+ "--port", type=int, default=8080,
35
+ help="Port to bind for HTTP mode (ignored for stdio)"
36
+ )
37
+
38
+ args = parser.parse_args()
39
+
40
+ if args.mode == "dev":
41
+ print("๐Ÿ‘จ๐Ÿ’ป Running in development (stdio) mode...", file=sys.stderr)
42
+ sys.stderr.flush()
43
+ run_server("stdio")
44
+ elif args.mode == "test":
45
+ print("๐Ÿงช Running in test mode... (placeholder)", file=sys.stderr)
46
+ # Insert mock or testing logic here
47
+ print("Simulated test complete.", file=sys.stderr)
48
+ sys.stderr.flush()
49
+ elif args.mode == "stdio":
50
+ print("๐Ÿ“Ÿ Explicit stdio mode...", file=sys.stderr)
51
+ sys.stderr.flush()
52
+ run_server("stdio")
53
+ elif args.mode == "http":
54
+ print(f"๐ŸŒ Running in HTTP mode on {args.host}:{args.port}...", file=sys.stderr)
55
+ sys.stderr.flush()
56
+ run_server("http", args.host, args.port)
57
+ else:
58
+ print(f"โš ๏ธ Unknown mode: {args.mode}", file=sys.stderr)
59
+ sys.stderr.flush()
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
microservice_ensure.py ADDED
@@ -0,0 +1,337 @@
1
+ # Copyright (c) 2025, Software Tree
2
+ # A utility to check the availability of an app-specific GIlhari microservice.
3
+ # Author: Damodar Periwal
4
+
5
+ """
6
+ # microservice_ensure.py
7
+
8
+ # A utility to check the availability of an app-specific GIlhari microservice.
9
+ # If the Gilhari microservice is not running on not responsive then this
10
+ # utility tries to start the service (through Docker) using the relevant
11
+ # parameter values read from environment variables as described below:
12
+
13
+ Env variables | description | default value
14
+
15
+ "GILHARI_NAME" | Name of the app-specific Gilhari microservice | "my-gilhari-microservice"
16
+ "GILHARI_HOST" | IP address of the host machine | "localhost"
17
+ "GILHARI_PORT" | Port number to contact the Gilhari microservice | 80
18
+ "GILHARI_IMAGE" | Docker image name of the app-specific Gilhari ! ""
19
+ microservice
20
+ """
21
+ import os
22
+ import requests
23
+ import socket
24
+ import subprocess
25
+ import time
26
+ import logging
27
+ from typing import Optional, List
28
+ from dataclasses import dataclass
29
+
30
+ # Configure logging
31
+ logging.basicConfig(level=logging.INFO)
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass
36
+ class ServiceConfig:
37
+ """Configuration for the microservice"""
38
+ name: str = ""
39
+ host: str = "localhost"
40
+ port: int = 80
41
+ health_endpoint: Optional[str] = "/health"
42
+ test_endpoint: Optional[str] = "/api/status"
43
+ docker_image: str = ""
44
+ docker_run_args: Optional[List[str]] = None
45
+ container_name: Optional[str] = None
46
+ startup_timeout: int = 60
47
+ health_check_interval: int = 2
48
+
49
+ def __post_init__(self):
50
+ if self.docker_run_args is None:
51
+ self.docker_run_args = []
52
+ if self.container_name is None:
53
+ self.container_name = f"{self.name}-container"
54
+
55
+
56
+ def check_health_endpoint(config: ServiceConfig) -> bool:
57
+ """
58
+ Method 1: Try health endpoint
59
+ """
60
+ if not config.health_endpoint:
61
+ logger.debug(f"No health endpoint configured for {config.name}")
62
+ return False
63
+
64
+ try:
65
+ url = f"http://{config.host}:{config.port}{config.health_endpoint}"
66
+ logger.debug(f"Checking health endpoint: {url}")
67
+
68
+ response = requests.get(url, timeout=5)
69
+ if response.status_code == 200:
70
+ logger.info(f"Health endpoint check passed for {config.name}")
71
+ return True
72
+ else:
73
+ logger.debug(f"Health endpoint returned status {response.status_code}")
74
+ return False
75
+
76
+ except requests.exceptions.RequestException as e:
77
+ logger.debug(f"Health endpoint check failed: {e}")
78
+ return False
79
+
80
+
81
+ def check_rest_api_endpoint(config: ServiceConfig) -> bool:
82
+ """
83
+ Method 2: Try if REST API service responds to a simple endpoint
84
+ """
85
+ if not config.test_endpoint:
86
+ logger.debug(f"No test endpoint configured for {config.name}")
87
+ return False
88
+
89
+ try:
90
+ url = f"http://{config.host}:{config.port}{config.test_endpoint}"
91
+ logger.debug(f"Checking REST API endpoint: {url}")
92
+
93
+ response = requests.get(url, timeout=5)
94
+
95
+ # Option 1: Accept any HTTP response (service is running)
96
+ # This treats 404, 500, etc. as "service is up"
97
+ logger.info(f"REST API endpoint check passed for {config.name} (status: {response.status_code})")
98
+ return True
99
+
100
+ # Option 2: Only accept successful responses (uncomment if you want this behavior)
101
+ # if 200 <= response.status_code < 400:
102
+ # logger.info(f"REST API endpoint check passed for {config.name} (status: {response.status_code})")
103
+ # return True
104
+ # else:
105
+ # logger.debug(f"REST API endpoint returned error status: {response.status_code}")
106
+ # return False
107
+
108
+ except requests.exceptions.RequestException as e:
109
+ logger.debug(f"REST API endpoint check failed: {e}")
110
+ return False
111
+
112
+
113
+ def check_port_connection(config: ServiceConfig) -> bool:
114
+ """
115
+ Method 3: Try port connection
116
+ """
117
+ try:
118
+ logger.debug(f"Checking port connection: {config.host}:{config.port}")
119
+
120
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
121
+ sock.settimeout(5)
122
+ result = sock.connect_ex((config.host, config.port))
123
+ sock.close()
124
+
125
+ if result == 0:
126
+ logger.info(f"Port connection check passed for {config.name}")
127
+ return True
128
+ else:
129
+ logger.debug(f"Port connection failed with result: {result}")
130
+ return False
131
+
132
+ except Exception as e:
133
+ logger.debug(f"Port connection check failed: {e}")
134
+ return False
135
+
136
+
137
+ def is_service_available(config: ServiceConfig) -> bool:
138
+ """
139
+ Test all three methods to check if service is available
140
+ """
141
+ logger.info(f"Checking availability of {config.name} service...")
142
+
143
+ # Method 1: Health endpoint
144
+ if check_health_endpoint(config):
145
+ return True
146
+
147
+ # Method 2: REST API endpoint
148
+ if check_rest_api_endpoint(config):
149
+ return True
150
+
151
+ # Method 3: Port connection
152
+ if check_port_connection(config):
153
+ return True
154
+
155
+ logger.info(f"Service {config.name} is not available")
156
+ return False
157
+
158
+
159
+ def stop_existing_container(config: ServiceConfig) -> bool:
160
+ """
161
+ Stop and remove existing container if it exists
162
+ """
163
+ try:
164
+ # Ensure container_name is not None
165
+ container_name = config.container_name or f"{config.name}-container"
166
+
167
+ # Check if container exists
168
+ result = subprocess.run(
169
+ ["docker", "ps", "-a", "--filter", f"name={container_name}", "--format", "{{.Names}}"],
170
+ capture_output=True,
171
+ text=True,
172
+ check=True
173
+ )
174
+
175
+ # Fix 1: Handle potential None result from stdout and container_name
176
+ if result.stdout and config.container_name and config.container_name in result.stdout:
177
+ logger.info(f"Stopping existing container: {config.container_name}")
178
+
179
+ # Stop container
180
+ subprocess.run(
181
+ ["docker", "stop", str(config.container_name)],
182
+ check=True
183
+ )
184
+
185
+ # Remove container
186
+ subprocess.run(
187
+ ["docker", "rm", str(config.container_name)],
188
+ check=True
189
+ )
190
+
191
+ logger.info(f"Removed existing container: {config.container_name}")
192
+ return True
193
+
194
+ except subprocess.CalledProcessError as e:
195
+ logger.warning(f"Error managing existing container: {e}")
196
+ return False
197
+
198
+ return False
199
+
200
+
201
+ def start_docker_service(config: ServiceConfig) -> bool:
202
+ """
203
+ Start the microservice using Docker
204
+ """
205
+ if not config.docker_image:
206
+ logger.error(f"No Docker image specified for {config.name}")
207
+ return False
208
+
209
+ try:
210
+ # Stop existing container if it exists
211
+ stop_existing_container(config)
212
+
213
+ # Build Docker run command
214
+ docker_cmd = [
215
+ "docker", "run",
216
+ "-d", # Run in detached mode
217
+ "--name", config.container_name
218
+ ]
219
+
220
+ # Add any additional Docker run arguments
221
+ docker_cmd.extend(config.docker_run_args or [])
222
+
223
+ # Add the image name
224
+ docker_cmd.append(config.docker_image)
225
+
226
+ logger.info(f"Starting Docker container for {config.name}: {' '.join(docker_cmd)}")
227
+
228
+ # Start the container
229
+ result = subprocess.run(docker_cmd, capture_output=True, text=True, check=True)
230
+ container_id = result.stdout.strip()
231
+
232
+ logger.info(f"Started container {config.container_name} with ID: {container_id}")
233
+ return True
234
+
235
+ except subprocess.CalledProcessError as e:
236
+ logger.error(f"Failed to start Docker container: {e}")
237
+ logger.error(f"stderr: {e.stderr}")
238
+ return False
239
+
240
+
241
+ def wait_for_service(config: ServiceConfig) -> bool:
242
+ """
243
+ Wait for the service to become available
244
+ """
245
+ logger.info(f"Waiting for {config.name} to become available...")
246
+
247
+ start_time = time.time()
248
+ while time.time() - start_time < config.startup_timeout:
249
+ if is_service_available(config):
250
+ elapsed = time.time() - start_time
251
+ logger.info(f"Service {config.name} is now available (took {elapsed:.1f}s)")
252
+ return True
253
+
254
+ logger.debug(f"Service not ready yet, waiting {config.health_check_interval}s...")
255
+ time.sleep(config.health_check_interval)
256
+
257
+ logger.error(f"Timeout waiting for {config.name} to become available")
258
+ return False
259
+
260
+
261
+ def ensure_microservice(config: ServiceConfig) -> bool:
262
+ """
263
+ Main function to ensure microservice is running and available
264
+
265
+ Args:
266
+ config: ServiceConfig object with service configuration
267
+
268
+ Returns:
269
+ bool: True if service is available, False otherwise
270
+ """
271
+ logger.info(f"Ensuring microservice {config.name} is available...")
272
+
273
+ # Check if service is already available
274
+ if is_service_available(config):
275
+ logger.info(f"Service {config.name} is already available")
276
+ return True
277
+
278
+ # Service is not available, try to start it
279
+ logger.info(f"Service {config.name} is not available, starting Docker container...")
280
+
281
+ if not start_docker_service(config):
282
+ logger.error(f"Failed to start Docker service for {config.name}")
283
+ return False
284
+
285
+ # Wait for service to become available
286
+ if not wait_for_service(config):
287
+ logger.error(f"Service {config.name} failed to become available after startup")
288
+ return False
289
+
290
+ logger.info(f"Successfully ensured {config.name} is available")
291
+ return True
292
+
293
+
294
+ # Ensure that the app-specific Gilhari microservice is running
295
+ def ensure_gilhari_service() -> bool:
296
+ """
297
+ Gather app-specific Gilhari microservice parameters and ensure its availability
298
+ """
299
+
300
+ GILHARI_INTERNAL_PORT = "8081" # fixed port number of Gilhari inside the container
301
+ portStr = os.getenv("GILHARI_PORT", "80") # Outside port number the container is listening on
302
+
303
+ # First gather the Gilhari microservice parameters
304
+ gilhari_service = ServiceConfig(
305
+ name=os.getenv("GILHARI_NAME", "my-gilhari-microservice"),
306
+ host=os.getenv("GILHARI_HOST", "localhost"),
307
+ port=int(portStr),
308
+ health_endpoint="/gilhari/v1/health/check",
309
+ test_endpoint="/gilhari/v1/api/status",
310
+ docker_image=os.getenv("GILHARI_IMAGE", ""), # don't want any default docker image name to cause confusion
311
+ docker_run_args=["-p", f"{portStr}:{GILHARI_INTERNAL_PORT}"],
312
+ startup_timeout=30,
313
+ health_check_interval=3
314
+ )
315
+
316
+ if not ensure_microservice(gilhari_service):
317
+ logger.error(f"Failed to ensure {gilhari_service.name} is available")
318
+ return False
319
+
320
+ return True
321
+
322
+
323
+ # Utility functions for testing individual components
324
+ def test_service_methods(config: ServiceConfig):
325
+ """
326
+ Test individual service checking methods
327
+ """
328
+ print(f"\nTesting service checking methods for {config.name}:")
329
+ print(f"Health endpoint check: {check_health_endpoint(config)}")
330
+ print(f"REST API endpoint check: {check_rest_api_endpoint(config)}")
331
+ print(f"Port connection check: {check_port_connection(config)}")
332
+ print(f"Overall availability: {is_service_available(config)}")
333
+
334
+
335
+ if __name__ == "__main__":
336
+ # Check for the availability of the Gilhari microservice
337
+ ensure_gilhari_service()