Skip to content

Commit dda2cfa

Browse files
authored
Merge branch 'master' into add-mseep-badge-911b94f0
2 parents bcf62d7 + f151731 commit dda2cfa

File tree

8 files changed

+144
-19
lines changed

8 files changed

+144
-19
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
<img width="380" height="200" src="https://glama.ai/mcp/servers/@caretdev/mcp-server-iris/badge" />
77
</a>
88

9+
[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/caretdev-mcp-server-iris-badge.png)](https://mseep.ai/app/caretdev-mcp-server-iris)
10+
11+
# mcp-server-iris: An InterSystems IRIS MCP server
12+
913
## Overview
1014

1115
A [Model Context Protocol](https://modelcontextprotocol.io/introduction) server for InterSystems IRIS database interaction and automation.

docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
database:
3+
image: containers.intersystems.com/intersystems/iris-community:2025.1
4+
restart: always
5+
ports:
6+
- "1972:1972"
7+
- "52773:52773"
8+
command:
9+
- -a
10+
- iris session iris -U%SYS '##class(Security.Users).UnExpireUserPasswords("*")'

example.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import sys
2+
import asyncio
3+
from typing import Optional
4+
from contextlib import AsyncExitStack
5+
6+
from mcp import ClientSession, StdioServerParameters
7+
from mcp.client.stdio import stdio_client
8+
9+
from dotenv import load_dotenv
10+
11+
load_dotenv() # load environment variables from .env
12+
13+
14+
class MCPClient:
15+
def __init__(self):
16+
# Initialize session and client objects
17+
self.session: Optional[ClientSession] = None
18+
self.exit_stack = AsyncExitStack()
19+
20+
async def connect_to_server(self):
21+
"""Connect to an MCP server"""
22+
server_params = StdioServerParameters(
23+
command=sys.executable, args=["-m", "mcp_server_iris"], env={
24+
"IRIS_HOSTNAME": "localhost",
25+
"IRIS_PORT": "1972",
26+
"IRIS_NAMESPACE": "USER",
27+
"IRIS_USERNAME": "_SYSTEM",
28+
"IRIS_PASSWORD": "SYS",
29+
}
30+
)
31+
# server_params = StdioServerParameters(
32+
# command="uvx", args=["."],
33+
# )
34+
35+
stdio_transport = await self.exit_stack.enter_async_context(
36+
stdio_client(server_params)
37+
)
38+
self.stdio, self.write = stdio_transport
39+
self.session = await self.exit_stack.enter_async_context(
40+
ClientSession(self.stdio, self.write)
41+
)
42+
43+
await self.session.initialize()
44+
45+
# List available tools
46+
response = await self.session.list_tools()
47+
tools = response.tools
48+
print(
49+
"\nConnected to server with tools:",
50+
"\n\t".join([""] + [tool.name for tool in tools]),
51+
)
52+
53+
async def process_query(self, query: str) -> str:
54+
response = await self.session.call_tool(
55+
"execute_sql",
56+
{
57+
"query": query,
58+
"params": [],
59+
},
60+
)
61+
print("Response from execute_sql:", response.content)
62+
return
63+
64+
async def cleanup(self):
65+
"""Clean up resources"""
66+
await self.exit_stack.aclose()
67+
68+
69+
async def main():
70+
client = MCPClient()
71+
try:
72+
await client.connect_to_server()
73+
await client.session.set_logging_level("debug")
74+
await client.process_query("select $namespace, $zversion")
75+
finally:
76+
await client.cleanup()
77+
78+
79+
if __name__ == "__main__":
80+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp-server-iris"
3-
version = "0.2.1"
3+
version = "0.3.1"
44
description = "A Model Context Protocol server for InterSystems IRIS."
55
readme = "README.md"
66
requires-python = ">=3.10"

pytest.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[pytest]
2+
asyncio_mode = auto
3+
asyncio_default_fixture_loop_scope = function
4+
testpaths = tests
5+
python_files = test_*.py

src/mcp_server_iris/mcpserver.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
from mcp.server.fastmcp.resources import ResourceManager
1313
from mcp.server.fastmcp import FastMCP
1414
from mcp.server.fastmcp import Context as FastMCPContext
15-
from mcp.shared.context import LifespanContextT
15+
from mcp.shared.context import LifespanContextT, RequestT
1616
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
1717

1818
logger = get_logger(__name__)
1919

2020
Context = FastMCPContext
2121

2222

23-
class MCPServerContext(Context, Generic[ServerSessionT, LifespanContextT]):
23+
class MCPServerContext(Context, Generic[ServerSessionT, LifespanContextT, RequestT]):
2424
def __init__(
2525
self,
2626
*,
@@ -98,6 +98,8 @@ def __init__(
9898
)
9999

100100
self.settings = Settings(**settings)
101+
configure_logging(self.settings.log_level)
102+
logger.setLevel(self.settings.log_level.upper())
101103

102104
self._tool_manager = ToolManager(
103105
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
@@ -110,8 +112,13 @@ def __init__(
110112
)
111113

112114
self._setup_handlers()
115+
self._mcp_server.set_logging_level()(self.set_logging_level)
113116

114-
configure_logging(self.settings.log_level)
117+
async def set_logging_level(self, level) -> None:
118+
"""Set the logging level for the server."""
119+
logger.info(f"Logging level set to {level}")
120+
logger.setLevel(level.upper())
121+
configure_logging(level=level)
115122

116123
@property
117124
def name(self) -> str:
@@ -138,7 +145,7 @@ def run(self, transport: Literal["stdio", "sse"] = "stdio") -> None:
138145
Args:
139146
transport: Transport protocol to use ("stdio" or "sse")
140147
"""
141-
148+
logger.info(f"Running server with transport: {transport}")
142149
TRANSPORTS = Literal["stdio", "sse"]
143150
if transport not in TRANSPORTS.__args__: # type: ignore
144151
raise ValueError(f"Unknown transport: {transport}")

src/mcp_server_iris/server.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,47 @@
11
import os
2-
import logging
32
from importlib.metadata import version
43
from contextlib import asynccontextmanager
54
from collections.abc import AsyncIterator
65
import mcp.types as types
76
import iris as irisnative
8-
from mcp_server_iris.mcpserver import MCPServer, Context
7+
from mcp_server_iris.mcpserver import MCPServer, Context, logger
98
from mcp_server_iris.interoperability import init as interoperability
109

11-
logger = logging.getLogger("mcp_server_iris")
1210
logger.info("Starting InterSystems IRIS MCP Server")
1311

1412

1513
def get_db_config():
1614
"""Get database configuration from environment variables."""
1715
config = {
18-
"hostname": os.getenv("IRIS_HOSTNAME", "localhost"),
16+
"hostname": os.getenv("IRIS_HOSTNAME"),
1917
"port": int(os.getenv("IRIS_PORT", 1972)),
20-
"namespace": os.getenv("IRIS_NAMESPACE", "USER"),
21-
"username": os.getenv("IRIS_USERNAME", "_SYSTEM"),
22-
"password": os.getenv("IRIS_PASSWORD", "SYS"),
18+
"namespace": os.getenv("IRIS_NAMESPACE"),
19+
"username": os.getenv("IRIS_USERNAME"),
20+
"password": os.getenv("IRIS_PASSWORD"),
2321
}
2422

25-
if not all([config["username"], config["password"], config["namespace"]]):
23+
if not all([config["hostname"], config["username"], config["password"], config["namespace"]]):
2624
raise ValueError("Missing required database configuration")
25+
logger.info(f"Server configuration: iris://{config["username"]}:{"x"*8}@{config["hostname"]}:{config["port"]}/{config["namespace"]}")
2726

2827
return config
2928

3029

3130
@asynccontextmanager
3231
async def server_lifespan(server: MCPServer) -> AsyncIterator[dict]:
3332
"""Manage server startup and shutdown lifecycle."""
34-
config = get_db_config()
3533
try:
36-
db = irisnative.connect(**config)
34+
config = get_db_config()
35+
except ValueError:
36+
yield {"db": None, "iris": None}
37+
return
38+
try:
39+
40+
db = irisnative.connect(sharedmemory=False, **config)
3741
iris = irisnative.createIRIS(db)
3842
yield {"db": db, "iris": iris}
39-
except Exception:
43+
except Exception as ex:
44+
logger.error(f"Error connecting to IRIS: {ex}")
4045
db = None
4146
iris = None
4247
yield {"db": db, "iris": iris}
@@ -189,7 +194,10 @@ def main():
189194
args = parser.parse_args()
190195
server.settings.port = args.port
191196
server.settings.debug = args.debug
192-
server.run(transport=args.transport)
197+
try:
198+
server.run(transport=args.transport)
199+
except KeyboardInterrupt:
200+
logger.info("Server stopped by user")
193201

194202

195203
if __name__ == "__main__":

tests/test_server.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
import pytest
3+
import json
34
from mcp.client.session import ClientSession
45
from mcp.client.stdio import StdioServerParameters, stdio_client
56
from mcp_server_iris.server import server
@@ -41,13 +42,23 @@ async def test_params_in_tool():
4142
@pytest.mark.asyncio
4243
async def test_call_tool_execute_sql(iris_config):
4344
"""Test calling execute_sql with a query."""
44-
async with stdio_client(StdioServerParameters(command=sys.executable, args=["-m", "mcp_server_iris"], env=iris_config)) as (read, write):
45+
async with stdio_client(
46+
StdioServerParameters(
47+
command=sys.executable, args=["-m", "mcp_server_iris"], env=iris_config
48+
)
49+
) as (read, write):
4550
async with ClientSession(read, write) as session:
4651
await session.initialize()
47-
_ = await session.call_tool(
52+
res = await session.call_tool(
4853
"execute_sql",
4954
{
5055
"query": "select $namespace, $zversion",
5156
"params": [],
5257
},
5358
)
59+
assert res is not None
60+
assert not res.isError
61+
assert res.content[0].type == "text"
62+
text = res.content[0].text
63+
assert len(text)
64+
print(f"Result: {text}")

0 commit comments

Comments
 (0)