integrum 0.1.0__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.
- integrum/__init__.py +19 -0
- integrum/cli.py +109 -0
- integrum/docstrings.py +103 -0
- integrum/filter.py +60 -0
- integrum/inspector.py +201 -0
- integrum/logo.png +0 -0
- integrum/logo_white.png +0 -0
- integrum/server.py +479 -0
- integrum/store.py +224 -0
- integrum-0.1.0.dist-info/METADATA +111 -0
- integrum-0.1.0.dist-info/RECORD +13 -0
- integrum-0.1.0.dist-info/WHEEL +4 -0
- integrum-0.1.0.dist-info/entry_points.txt +2 -0
integrum/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Integrum: Expose any Python module or library as an MCP server."""
|
|
2
|
+
|
|
3
|
+
from integrum.filter import MemberFilter
|
|
4
|
+
from integrum.inspector import import_target, inspect_class, inspect_function, inspect_module
|
|
5
|
+
from integrum.server import IntegrumServer
|
|
6
|
+
from integrum.store import ObjectStore, WordIdGenerator
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"IntegrumServer",
|
|
12
|
+
"ObjectStore",
|
|
13
|
+
"WordIdGenerator",
|
|
14
|
+
"MemberFilter",
|
|
15
|
+
"import_target",
|
|
16
|
+
"inspect_class",
|
|
17
|
+
"inspect_function",
|
|
18
|
+
"inspect_module",
|
|
19
|
+
]
|
integrum/cli.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Command line interface for Integrum."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from integrum.filter import MemberFilter
|
|
8
|
+
from integrum.inspector import import_target, inspect_class, inspect_function, inspect_module
|
|
9
|
+
from integrum.server import IntegrumServer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.version_option()
|
|
14
|
+
def main() -> None:
|
|
15
|
+
"""Integrum: Expose Python modules as Model Context Protocol (MCP) servers."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@main.command("expose")
|
|
20
|
+
@click.argument("targets", nargs=-1, required=True)
|
|
21
|
+
@click.option("--name", default="integrum-server", help="Name of the MCP server.")
|
|
22
|
+
@click.option("--include", "-i", multiple=True, help="Regex pattern(s) to include.")
|
|
23
|
+
@click.option("--exclude", "-e", multiple=True, help="Regex pattern(s) to exclude.")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--transport",
|
|
26
|
+
"-t",
|
|
27
|
+
type=click.Choice(["stdio", "http", "sse"], case_sensitive=False),
|
|
28
|
+
default="stdio",
|
|
29
|
+
help="MCP transport protocol (default: stdio).",
|
|
30
|
+
)
|
|
31
|
+
@click.option("--host", default="127.0.0.1", help="Host to bind for HTTP/SSE (default: 127.0.0.1).")
|
|
32
|
+
@click.option("--port", "-p", default=8000, type=int, help="Port to bind for HTTP/SSE (default: 8000).")
|
|
33
|
+
def expose_cmd(
|
|
34
|
+
targets: List[str],
|
|
35
|
+
name: str,
|
|
36
|
+
include: List[str],
|
|
37
|
+
exclude: List[str],
|
|
38
|
+
transport: str,
|
|
39
|
+
host: str,
|
|
40
|
+
port: int,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Expose TARGETS (modules, classes, or functions) as an MCP server."""
|
|
43
|
+
server = IntegrumServer(name=name)
|
|
44
|
+
inc_list = list(include) if include else None
|
|
45
|
+
exc_list = list(exclude) if exclude else None
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
registered = server.expose(list(targets), include=inc_list, exclude=exc_list)
|
|
49
|
+
if not registered:
|
|
50
|
+
click.echo("Warning: No tools were registered. Check your targets or filter patterns.", err=True)
|
|
51
|
+
else:
|
|
52
|
+
click.echo(f"Successfully registered {len(registered)} tools: {', '.join(registered)}", err=True)
|
|
53
|
+
|
|
54
|
+
click.echo(f"Starting Integrum MCP server '{name}' via transport={transport}...", err=True)
|
|
55
|
+
server.run(transport=transport, host=host, port=port)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
click.echo(f"Error starting Integrum server: {e}", err=True)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@main.command("list")
|
|
62
|
+
@click.argument("targets", nargs=-1, required=True)
|
|
63
|
+
@click.option("--include", "-i", multiple=True, help="Regex pattern(s) to include.")
|
|
64
|
+
@click.option("--exclude", "-e", multiple=True, help="Regex pattern(s) to exclude.")
|
|
65
|
+
def list_cmd(targets: List[str], include: List[str], exclude: List[str]) -> None:
|
|
66
|
+
"""Preview the functions and classes that would be exposed from TARGETS."""
|
|
67
|
+
inc_list = list(include) if include else None
|
|
68
|
+
exc_list = list(exclude) if exclude else None
|
|
69
|
+
member_filter = MemberFilter(include=inc_list, exclude=exc_list) if (inc_list or exc_list) else None
|
|
70
|
+
|
|
71
|
+
for target_str in targets:
|
|
72
|
+
try:
|
|
73
|
+
obj = import_target(target_str)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
click.echo(f"Error importing '{target_str}': {e}", err=True)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
click.echo(f"\nTarget: {target_str} ({type(obj).__name__})")
|
|
79
|
+
click.echo("-" * 40)
|
|
80
|
+
|
|
81
|
+
import inspect
|
|
82
|
+
if inspect.isfunction(obj) or inspect.isbuiltin(obj):
|
|
83
|
+
fn_info = inspect_function(obj)
|
|
84
|
+
click.echo(f" [Function] {fn_info.name}{fn_info.signature}")
|
|
85
|
+
if fn_info.docstring:
|
|
86
|
+
first_line = fn_info.docstring.strip().split("\n")[0]
|
|
87
|
+
click.echo(f" Doc: {first_line}")
|
|
88
|
+
elif inspect.isclass(obj):
|
|
89
|
+
cls_info = inspect_class(obj, member_filter=member_filter)
|
|
90
|
+
click.echo(f" [Class] {cls_info.name}")
|
|
91
|
+
click.echo(f" Constructor: {cls_info.name}_create{cls_info.init_signature}")
|
|
92
|
+
for m_name, m_info in cls_info.methods.items():
|
|
93
|
+
click.echo(f" Method: {cls_info.name}_{m_name}{m_info.signature}")
|
|
94
|
+
elif inspect.ismodule(obj):
|
|
95
|
+
mod_info = inspect_module(obj, member_filter=member_filter)
|
|
96
|
+
if mod_info.functions:
|
|
97
|
+
click.echo(" Functions:")
|
|
98
|
+
for fn_name, fn_info in mod_info.functions.items():
|
|
99
|
+
click.echo(f" - {fn_name}{fn_info.signature}")
|
|
100
|
+
if mod_info.classes:
|
|
101
|
+
click.echo(" Classes:")
|
|
102
|
+
for c_name, c_info in mod_info.classes.items():
|
|
103
|
+
click.echo(f" - {c_name} ({len(c_info.methods)} methods)")
|
|
104
|
+
for m_name in c_info.methods:
|
|
105
|
+
click.echo(f" * {c_name}_{m_name}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
integrum/docstrings.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Docstring parsing and instruction injection for Integrum FastMCP tools."""
|
|
2
|
+
|
|
3
|
+
import docstring_parser
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SERVER_INSTRUCTIONS = (
|
|
8
|
+
"This MCP server is dynamically generated by Integrum.\n"
|
|
9
|
+
"- State Management: Non-primitive Python objects (such as model instances, datasets, "
|
|
10
|
+
"and arrays) are stored in memory and assigned memorable word IDs (e.g., 'playful_llama', 'swift_falcon').\n"
|
|
11
|
+
"- Using Objects: When a tool returns an object ID, supply that ID to subsequent tools operating on it. "
|
|
12
|
+
"For class methods, pass it as the 'obj_id' parameter. "
|
|
13
|
+
"You can also use dot-notation for sub-attributes (e.g., 'swift_falcon.data' and 'swift_falcon.target')."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_function_description(
|
|
18
|
+
raw_doc: Optional[str],
|
|
19
|
+
is_constructor: bool = False,
|
|
20
|
+
is_method: bool = False,
|
|
21
|
+
class_name: Optional[str] = None,
|
|
22
|
+
function_name: Optional[str] = None,
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Format and enhance tool description with injected instructions."""
|
|
25
|
+
summary_parts = []
|
|
26
|
+
param_docs: Dict[str, str] = {}
|
|
27
|
+
|
|
28
|
+
if raw_doc:
|
|
29
|
+
try:
|
|
30
|
+
parsed = docstring_parser.parse(raw_doc)
|
|
31
|
+
if parsed.short_description:
|
|
32
|
+
summary_parts.append(parsed.short_description)
|
|
33
|
+
if parsed.long_description:
|
|
34
|
+
# Add first 1-2 paragraphs of long description if reasonable
|
|
35
|
+
long_desc = parsed.long_description.strip()
|
|
36
|
+
if len(long_desc) > 500:
|
|
37
|
+
long_desc = long_desc[:500] + "..."
|
|
38
|
+
summary_parts.append(long_desc)
|
|
39
|
+
|
|
40
|
+
for param in parsed.params:
|
|
41
|
+
if param.arg_name and param.description:
|
|
42
|
+
param_docs[param.arg_name] = param.description.strip()
|
|
43
|
+
except Exception:
|
|
44
|
+
summary_parts.append(raw_doc.strip()[:400])
|
|
45
|
+
else:
|
|
46
|
+
if is_constructor:
|
|
47
|
+
summary_parts.append(f"Create an instance of {class_name}.")
|
|
48
|
+
elif is_method:
|
|
49
|
+
summary_parts.append(f"Execute method on {class_name} instance.")
|
|
50
|
+
else:
|
|
51
|
+
summary_parts.append("Execute Python function.")
|
|
52
|
+
|
|
53
|
+
description_text = "\n\n".join(summary_parts)
|
|
54
|
+
|
|
55
|
+
# Injected state management instructions
|
|
56
|
+
injected_instructions = []
|
|
57
|
+
if is_constructor:
|
|
58
|
+
injected_instructions.append(
|
|
59
|
+
"[STATE INSTRUCTION]: Calling this creates a new instance and returns an object ID "
|
|
60
|
+
"(e.g. 'playful_llama'). Store this ID to pass as 'obj_id' in subsequent method calls."
|
|
61
|
+
)
|
|
62
|
+
elif is_method:
|
|
63
|
+
injected_instructions.append(
|
|
64
|
+
"[STATE INSTRUCTION]: Requires 'obj_id' referencing an existing instance. "
|
|
65
|
+
"Arguments accepting datasets/arrays can accept registered object IDs or dot-notation "
|
|
66
|
+
"(e.g. 'swift_falcon.data')."
|
|
67
|
+
)
|
|
68
|
+
elif function_name == "train_test_split":
|
|
69
|
+
injected_instructions.append(
|
|
70
|
+
"[STATE INSTRUCTION]: Pass arrays to split as a list, e.g. arrays=['<id>.data', '<id>.target'] "
|
|
71
|
+
"(or arrays=['<id>_0', '<id>_1']), or X='...' and y='...'. "
|
|
72
|
+
"Returns 4 arrays: X_train, X_test, y_train, y_test, which can be passed to subsequent tools."
|
|
73
|
+
)
|
|
74
|
+
elif function_name == "accuracy_score":
|
|
75
|
+
injected_instructions.append(
|
|
76
|
+
"[STATE INSTRUCTION]: Pass y_true and y_pred as object IDs (e.g. y_true='<split_id>.y_test', "
|
|
77
|
+
"y_pred='<predictions_id>'). Returns accuracy float."
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
injected_instructions.append(
|
|
81
|
+
"[STATE INSTRUCTION]: If this function returns a dataset or complex object, it will be "
|
|
82
|
+
"registered in memory with a word ID. Arguments can also accept registered object IDs."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if injected_instructions:
|
|
86
|
+
description_text += "\n\n" + "\n".join(injected_instructions)
|
|
87
|
+
|
|
88
|
+
return description_text
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def extract_parameter_descriptions(raw_doc: Optional[str]) -> Dict[str, str]:
|
|
92
|
+
"""Extract individual parameter descriptions from docstrings."""
|
|
93
|
+
if not raw_doc:
|
|
94
|
+
return {}
|
|
95
|
+
param_docs = {}
|
|
96
|
+
try:
|
|
97
|
+
parsed = docstring_parser.parse(raw_doc)
|
|
98
|
+
for param in parsed.params:
|
|
99
|
+
if param.arg_name and param.description:
|
|
100
|
+
param_docs[param.arg_name] = param.description.strip()
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
return param_docs
|
integrum/filter.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Regex-based filtering for module members and class methods."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional, Pattern, Union, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MemberFilter:
|
|
8
|
+
"""Filters symbols (functions, classes, methods) using regex patterns."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
include: Optional[Union[str, List[str]]] = None,
|
|
13
|
+
exclude: Optional[Union[str, List[str]]] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
self.include_patterns: List[Pattern[str]] = self._compile_patterns(include)
|
|
16
|
+
self.exclude_patterns: List[Pattern[str]] = self._compile_patterns(exclude)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _compile_patterns(patterns: Optional[Union[str, List[str]]]) -> List[Pattern[str]]:
|
|
20
|
+
if not patterns:
|
|
21
|
+
return []
|
|
22
|
+
if isinstance(patterns, str):
|
|
23
|
+
patterns = [patterns]
|
|
24
|
+
compiled = []
|
|
25
|
+
for pat in patterns:
|
|
26
|
+
if pat:
|
|
27
|
+
try:
|
|
28
|
+
compiled.append(re.compile(pat))
|
|
29
|
+
except re.error as e:
|
|
30
|
+
raise ValueError(f"Invalid regex pattern '{pat}': {e}") from e
|
|
31
|
+
return compiled
|
|
32
|
+
|
|
33
|
+
def matches(self, name: str, qualified_name: Optional[str] = None) -> bool:
|
|
34
|
+
"""Check if a symbol name passes include/exclude rules.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
name: Simple name (e.g. 'fit', 'load_iris')
|
|
38
|
+
qualified_name: Full name (e.g. 'RandomForestClassifier.fit')
|
|
39
|
+
"""
|
|
40
|
+
candidates = [name]
|
|
41
|
+
if qualified_name and qualified_name != name:
|
|
42
|
+
candidates.append(qualified_name)
|
|
43
|
+
|
|
44
|
+
# Check excludes first
|
|
45
|
+
for pat in self.exclude_patterns:
|
|
46
|
+
for cand in candidates:
|
|
47
|
+
if pat.search(cand):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
# If no includes are specified, everything non-excluded passes
|
|
51
|
+
if not self.include_patterns:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
# Check includes
|
|
55
|
+
for pat in self.include_patterns:
|
|
56
|
+
for cand in candidates:
|
|
57
|
+
if pat.search(cand):
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
return False
|
integrum/inspector.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Dynamic module, function, and class introspection using inspect."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
|
7
|
+
|
|
8
|
+
from integrum.filter import MemberFilter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class FunctionInfo:
|
|
13
|
+
"""Introspected information about a function or static method."""
|
|
14
|
+
name: str
|
|
15
|
+
target: Callable[..., Any]
|
|
16
|
+
signature: inspect.Signature
|
|
17
|
+
docstring: Optional[str]
|
|
18
|
+
is_coroutine: bool = False
|
|
19
|
+
source_module: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class MethodInfo:
|
|
24
|
+
"""Introspected information about a class method."""
|
|
25
|
+
name: str
|
|
26
|
+
method: Callable[..., Any]
|
|
27
|
+
signature: inspect.Signature
|
|
28
|
+
docstring: Optional[str]
|
|
29
|
+
is_static: bool = False
|
|
30
|
+
is_class_method: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ClassInfo:
|
|
35
|
+
"""Introspected information about a class."""
|
|
36
|
+
name: str
|
|
37
|
+
cls: Type[Any]
|
|
38
|
+
init_signature: inspect.Signature
|
|
39
|
+
docstring: Optional[str]
|
|
40
|
+
methods: Dict[str, MethodInfo] = field(default_factory=dict)
|
|
41
|
+
source_module: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ModuleInfo:
|
|
46
|
+
"""Introspected information about a module."""
|
|
47
|
+
name: str
|
|
48
|
+
functions: Dict[str, FunctionInfo] = field(default_factory=dict)
|
|
49
|
+
classes: Dict[str, ClassInfo] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def import_target(target: Union[str, Any]) -> Any:
|
|
53
|
+
"""Import a target string or return the object if already imported.
|
|
54
|
+
|
|
55
|
+
Supports:
|
|
56
|
+
- 'module' (e.g. 'math')
|
|
57
|
+
- 'module.submodule' (e.g. 'sklearn.datasets')
|
|
58
|
+
- 'module.submodule.symbol' (e.g. 'sklearn.datasets.load_iris')
|
|
59
|
+
- 'module:symbol' (e.g. 'sklearn.datasets:load_iris')
|
|
60
|
+
"""
|
|
61
|
+
if not isinstance(target, str):
|
|
62
|
+
return target
|
|
63
|
+
|
|
64
|
+
# Handle colon notation
|
|
65
|
+
if ":" in target:
|
|
66
|
+
mod_name, attr_name = target.split(":", 1)
|
|
67
|
+
mod = importlib.import_module(mod_name)
|
|
68
|
+
curr = mod
|
|
69
|
+
for part in attr_name.split("."):
|
|
70
|
+
curr = getattr(curr, part)
|
|
71
|
+
return curr
|
|
72
|
+
|
|
73
|
+
# Try importing directly as module
|
|
74
|
+
try:
|
|
75
|
+
return importlib.import_module(target)
|
|
76
|
+
except ModuleNotFoundError:
|
|
77
|
+
# If last part is an attribute of a module
|
|
78
|
+
if "." in target:
|
|
79
|
+
parts = target.rsplit(".", 1)
|
|
80
|
+
try:
|
|
81
|
+
mod = importlib.import_module(parts[0])
|
|
82
|
+
if hasattr(mod, parts[1]):
|
|
83
|
+
return getattr(mod, parts[1])
|
|
84
|
+
except ModuleNotFoundError:
|
|
85
|
+
pass
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def inspect_function(func: Callable[..., Any], name: Optional[str] = None) -> FunctionInfo:
|
|
90
|
+
"""Inspect a function using the inspect module."""
|
|
91
|
+
fn_name = name or getattr(func, "__name__", "anonymous_func")
|
|
92
|
+
try:
|
|
93
|
+
sig = inspect.signature(func)
|
|
94
|
+
except (ValueError, TypeError):
|
|
95
|
+
sig = inspect.Signature()
|
|
96
|
+
|
|
97
|
+
doc = inspect.getdoc(func)
|
|
98
|
+
is_coro = inspect.iscoroutinefunction(func)
|
|
99
|
+
mod = getattr(func, "__module__", None)
|
|
100
|
+
|
|
101
|
+
return FunctionInfo(
|
|
102
|
+
name=fn_name,
|
|
103
|
+
target=func,
|
|
104
|
+
signature=sig,
|
|
105
|
+
docstring=doc,
|
|
106
|
+
is_coroutine=is_coro,
|
|
107
|
+
source_module=mod,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def inspect_class(
|
|
112
|
+
cls: Type[Any],
|
|
113
|
+
name: Optional[str] = None,
|
|
114
|
+
member_filter: Optional[MemberFilter] = None,
|
|
115
|
+
) -> ClassInfo:
|
|
116
|
+
"""Inspect a class, its constructor, and public methods."""
|
|
117
|
+
cls_name = name or cls.__name__
|
|
118
|
+
doc = inspect.getdoc(cls)
|
|
119
|
+
mod = getattr(cls, "__module__", None)
|
|
120
|
+
|
|
121
|
+
# Constructor signature
|
|
122
|
+
try:
|
|
123
|
+
init_sig = inspect.signature(cls)
|
|
124
|
+
except (ValueError, TypeError):
|
|
125
|
+
try:
|
|
126
|
+
init_sig = inspect.signature(cls.__init__)
|
|
127
|
+
except (ValueError, TypeError):
|
|
128
|
+
init_sig = inspect.Signature()
|
|
129
|
+
|
|
130
|
+
methods: Dict[str, MethodInfo] = {}
|
|
131
|
+
|
|
132
|
+
for attr_name, member in inspect.getmembers(cls):
|
|
133
|
+
# Skip private/dunder methods
|
|
134
|
+
if attr_name.startswith("_"):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Check filter if provided
|
|
138
|
+
if member_filter and not member_filter.matches(attr_name, f"{cls_name}.{attr_name}"):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Distinguish functions/methods
|
|
142
|
+
if inspect.isfunction(member) or inspect.ismethod(member) or inspect.isbuiltin(member):
|
|
143
|
+
try:
|
|
144
|
+
sig = inspect.signature(member)
|
|
145
|
+
except (ValueError, TypeError):
|
|
146
|
+
sig = inspect.Signature()
|
|
147
|
+
|
|
148
|
+
method_doc = inspect.getdoc(member)
|
|
149
|
+
# Check static/classmethod attributes
|
|
150
|
+
raw_attr = getattr(cls, attr_name, member)
|
|
151
|
+
is_static = isinstance(raw_attr, staticmethod)
|
|
152
|
+
is_class = isinstance(raw_attr, classmethod)
|
|
153
|
+
|
|
154
|
+
methods[attr_name] = MethodInfo(
|
|
155
|
+
name=attr_name,
|
|
156
|
+
method=member,
|
|
157
|
+
signature=sig,
|
|
158
|
+
docstring=method_doc,
|
|
159
|
+
is_static=is_static,
|
|
160
|
+
is_class_method=is_class,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return ClassInfo(
|
|
164
|
+
name=cls_name,
|
|
165
|
+
cls=cls,
|
|
166
|
+
init_signature=init_sig,
|
|
167
|
+
docstring=doc,
|
|
168
|
+
methods=methods,
|
|
169
|
+
source_module=mod,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def inspect_module(
|
|
174
|
+
module: Any,
|
|
175
|
+
member_filter: Optional[MemberFilter] = None,
|
|
176
|
+
) -> ModuleInfo:
|
|
177
|
+
"""Inspect an entire module for public functions and classes."""
|
|
178
|
+
mod_name = getattr(module, "__name__", str(module))
|
|
179
|
+
functions: Dict[str, FunctionInfo] = {}
|
|
180
|
+
classes: Dict[str, ClassInfo] = {}
|
|
181
|
+
|
|
182
|
+
for attr_name, member in inspect.getmembers(module):
|
|
183
|
+
# Skip private members
|
|
184
|
+
if attr_name.startswith("_"):
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# Respect filter
|
|
188
|
+
if member_filter and not member_filter.matches(attr_name, f"{mod_name}.{attr_name}"):
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
if inspect.isclass(member):
|
|
192
|
+
# Check if class was defined in this module or related package
|
|
193
|
+
classes[attr_name] = inspect_class(member, name=attr_name, member_filter=member_filter)
|
|
194
|
+
elif inspect.isfunction(member) or inspect.isbuiltin(member):
|
|
195
|
+
functions[attr_name] = inspect_function(member, name=attr_name)
|
|
196
|
+
|
|
197
|
+
return ModuleInfo(
|
|
198
|
+
name=mod_name,
|
|
199
|
+
functions=functions,
|
|
200
|
+
classes=classes,
|
|
201
|
+
)
|
integrum/logo.png
ADDED
|
Binary file
|
integrum/logo_white.png
ADDED
|
Binary file
|
integrum/server.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""FastMCP server integration and dynamic tool generation for Integrum."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP, Context
|
|
9
|
+
from fastmcp.server.server import Transport
|
|
10
|
+
|
|
11
|
+
from integrum.docstrings import (
|
|
12
|
+
SERVER_INSTRUCTIONS,
|
|
13
|
+
format_function_description,
|
|
14
|
+
extract_parameter_descriptions,
|
|
15
|
+
)
|
|
16
|
+
from integrum.filter import MemberFilter
|
|
17
|
+
from integrum.inspector import (
|
|
18
|
+
ClassInfo,
|
|
19
|
+
FunctionInfo,
|
|
20
|
+
import_target,
|
|
21
|
+
inspect_class,
|
|
22
|
+
inspect_function,
|
|
23
|
+
inspect_module,
|
|
24
|
+
)
|
|
25
|
+
from integrum.store import ObjectStore
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("integrum")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_session_id(ctx: Optional[Context]) -> str:
|
|
31
|
+
"""Extract session ID safely from FastMCP Context, or fallback to 'default'."""
|
|
32
|
+
if ctx is not None:
|
|
33
|
+
try:
|
|
34
|
+
sid = getattr(ctx, "session_id", None)
|
|
35
|
+
if sid:
|
|
36
|
+
return str(sid)
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
return "default"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_parameter_list(
|
|
43
|
+
sig: inspect.Signature,
|
|
44
|
+
skip_first: bool = False,
|
|
45
|
+
extra_lead_params: Optional[List[Tuple[str, str]]] = None,
|
|
46
|
+
) -> Tuple[List[str], List[Tuple[str, str, Any, bool]], Optional[str]]:
|
|
47
|
+
"""Parse signature parameters into executable code definitions and metadata.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
(param_code_list, param_meta_list, var_pos_param_name)
|
|
51
|
+
"""
|
|
52
|
+
param_code_list: List[str] = []
|
|
53
|
+
param_meta_list: List[Tuple[str, str, Any, bool]] = []
|
|
54
|
+
has_kwonly_marker = False
|
|
55
|
+
var_pos_param_name: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
if extra_lead_params:
|
|
58
|
+
for name, ann in extra_lead_params:
|
|
59
|
+
param_code_list.append(f"{name}: {ann}")
|
|
60
|
+
param_meta_list.append((name, ann, inspect.Parameter.empty, False))
|
|
61
|
+
|
|
62
|
+
for i, (name, param) in enumerate(sig.parameters.items()):
|
|
63
|
+
if name in ("self", "cls"):
|
|
64
|
+
continue
|
|
65
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
66
|
+
continue
|
|
67
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
68
|
+
var_pos_param_name = name
|
|
69
|
+
param_code_list.append(f"{name}: Any = None")
|
|
70
|
+
param_meta_list.append((name, "Any", None, True))
|
|
71
|
+
# If named arrays/args, also support X and y as optional aliases
|
|
72
|
+
if name in ("arrays", "args") and "X" not in sig.parameters and "y" not in sig.parameters:
|
|
73
|
+
param_code_list.append("X: Any = None")
|
|
74
|
+
param_meta_list.append(("X", "Any", None, True))
|
|
75
|
+
param_code_list.append("y: Any = None")
|
|
76
|
+
param_meta_list.append(("y", "Any", None, True))
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if param.kind == inspect.Parameter.KEYWORD_ONLY and not has_kwonly_marker:
|
|
80
|
+
param_code_list.append("*")
|
|
81
|
+
has_kwonly_marker = True
|
|
82
|
+
|
|
83
|
+
default = param.default
|
|
84
|
+
has_default = default is not inspect.Parameter.empty
|
|
85
|
+
|
|
86
|
+
ann = "Any"
|
|
87
|
+
if name == "obj_id":
|
|
88
|
+
ann = "str"
|
|
89
|
+
elif param.annotation in (int, float, str, bool):
|
|
90
|
+
ann = param.annotation.__name__
|
|
91
|
+
|
|
92
|
+
if has_default:
|
|
93
|
+
if isinstance(default, (int, float, str, bool, type(None))):
|
|
94
|
+
def_repr = repr(default)
|
|
95
|
+
else:
|
|
96
|
+
def_repr = "None"
|
|
97
|
+
param_code_list.append(f"{name}: {ann} = {def_repr}")
|
|
98
|
+
param_meta_list.append((name, ann, default, True))
|
|
99
|
+
else:
|
|
100
|
+
param_code_list.append(f"{name}: {ann}")
|
|
101
|
+
param_meta_list.append((name, ann, inspect.Parameter.empty, False))
|
|
102
|
+
|
|
103
|
+
return param_code_list, param_meta_list, var_pos_param_name
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _create_dynamic_wrapper(
|
|
107
|
+
tool_name: str,
|
|
108
|
+
docstring: str,
|
|
109
|
+
param_code_list: List[str],
|
|
110
|
+
handler: Callable[[Dict[str, Any], Optional[Context]], Any],
|
|
111
|
+
) -> Callable[..., Any]:
|
|
112
|
+
"""Create a typed dynamic function compatible with FastMCP inspection."""
|
|
113
|
+
full_params = list(param_code_list) + ["ctx: Context = None"]
|
|
114
|
+
param_str = ", ".join(full_params)
|
|
115
|
+
|
|
116
|
+
code = f"""
|
|
117
|
+
def {tool_name}({param_str}) -> Any:
|
|
118
|
+
kw = locals().copy()
|
|
119
|
+
kw.pop('ctx', None)
|
|
120
|
+
return __handler(kw, ctx)
|
|
121
|
+
"""
|
|
122
|
+
local_ns: Dict[str, Any] = {
|
|
123
|
+
"Context": Context,
|
|
124
|
+
"Any": Any,
|
|
125
|
+
"__handler": handler,
|
|
126
|
+
}
|
|
127
|
+
exec(code, local_ns)
|
|
128
|
+
wrapper = local_ns[tool_name]
|
|
129
|
+
wrapper.__name__ = tool_name
|
|
130
|
+
wrapper.__doc__ = docstring
|
|
131
|
+
return wrapper
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _format_result(
|
|
135
|
+
result: Any,
|
|
136
|
+
store: ObjectStore,
|
|
137
|
+
session_id: str,
|
|
138
|
+
label: str,
|
|
139
|
+
target_obj_id: Optional[str] = None,
|
|
140
|
+
) -> Any:
|
|
141
|
+
"""Format execution output, registering non-primitive objects in the store."""
|
|
142
|
+
# If method returned self (like estimator.fit())
|
|
143
|
+
if target_obj_id is not None and result is store.get(target_obj_id, session_id=session_id):
|
|
144
|
+
return {
|
|
145
|
+
"status": "success",
|
|
146
|
+
"object_id": target_obj_id,
|
|
147
|
+
"message": f"Successfully executed on object '{target_obj_id}' (returned self).",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# If result is primitive
|
|
151
|
+
if result is None:
|
|
152
|
+
return {"status": "success", "message": "Execution completed successfully (None)."}
|
|
153
|
+
if isinstance(result, (int, float, bool)):
|
|
154
|
+
return result
|
|
155
|
+
if isinstance(result, str) and len(result) < 500:
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
# Non-primitive object (numpy array, model, Bunch, dict, dataset, etc.)
|
|
159
|
+
new_id = store.register(result, label=label, session_id=session_id)
|
|
160
|
+
|
|
161
|
+
meta = store.list_objects(session_id=session_id).get(new_id, {})
|
|
162
|
+
response: Dict[str, Any] = {
|
|
163
|
+
"status": "success",
|
|
164
|
+
"object_id": new_id,
|
|
165
|
+
"type": meta.get("type", type(result).__name__),
|
|
166
|
+
}
|
|
167
|
+
if meta.get("shape"):
|
|
168
|
+
response["shape"] = meta["shape"]
|
|
169
|
+
|
|
170
|
+
# If composite (tuple/dict/bunch)
|
|
171
|
+
if isinstance(result, (tuple, list)):
|
|
172
|
+
response["items"] = [f"{new_id}_{i}" for i in range(len(result))]
|
|
173
|
+
if len(result) == 4 and "split" in label.lower():
|
|
174
|
+
sub_labels = ["X_train", "X_test", "y_train", "y_test"]
|
|
175
|
+
for idx, sub in enumerate(sub_labels):
|
|
176
|
+
store._sessions[session_id][f"{new_id}.{sub}"] = result[idx]
|
|
177
|
+
store._sessions[session_id][f"{new_id}_{sub}"] = result[idx]
|
|
178
|
+
response["sub_keys"] = [f"{new_id}.{sub}" for sub in sub_labels]
|
|
179
|
+
response["message"] = (
|
|
180
|
+
f"train_test_split returned 4 arrays stored under '{new_id}':\n"
|
|
181
|
+
f" - X_train: '{new_id}.X_train' (or '{new_id}_0')\n"
|
|
182
|
+
f" - X_test: '{new_id}.X_test' (or '{new_id}_1')\n"
|
|
183
|
+
f" - y_train: '{new_id}.y_train' (or '{new_id}_2')\n"
|
|
184
|
+
f" - y_test: '{new_id}.y_test' (or '{new_id}_3')"
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
response["message"] = (
|
|
188
|
+
f"Stored {type(result).__name__} with object_id '{new_id}'. "
|
|
189
|
+
f"Access items using: {response['items']}."
|
|
190
|
+
)
|
|
191
|
+
elif isinstance(result, dict) or hasattr(result, "keys"):
|
|
192
|
+
try:
|
|
193
|
+
keys = list(result.keys())[:10]
|
|
194
|
+
response["sub_keys"] = [f"{new_id}.{k}" for k in keys]
|
|
195
|
+
response["message"] = (
|
|
196
|
+
f"Stored {type(result).__name__} with object_id '{new_id}'. "
|
|
197
|
+
f"Access sub-keys using dot notation: {response['sub_keys']}."
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
response["message"] = f"Stored {type(result).__name__} with object_id '{new_id}'."
|
|
201
|
+
else:
|
|
202
|
+
response["message"] = f"Stored {type(result).__name__} with object_id '{new_id}'."
|
|
203
|
+
|
|
204
|
+
return response
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class IntegrumServer:
|
|
208
|
+
"""Converts Python modules, classes, and functions into a FastMCP server with state management."""
|
|
209
|
+
|
|
210
|
+
def __init__(
|
|
211
|
+
self,
|
|
212
|
+
name: str = "integrum-server",
|
|
213
|
+
store: Optional[ObjectStore] = None,
|
|
214
|
+
instructions: Optional[str] = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
self.name = name
|
|
217
|
+
self.store = store or ObjectStore()
|
|
218
|
+
self.instructions = instructions or SERVER_INSTRUCTIONS
|
|
219
|
+
self.mcp = FastMCP(name=self.name, instructions=self.instructions)
|
|
220
|
+
self._registered_tool_names: set[str] = set()
|
|
221
|
+
|
|
222
|
+
# Register utility tools
|
|
223
|
+
self._register_utility_tools()
|
|
224
|
+
|
|
225
|
+
def _register_utility_tools(self) -> None:
|
|
226
|
+
"""Register built-in utility tools for inspecting stored objects."""
|
|
227
|
+
store = self.store
|
|
228
|
+
|
|
229
|
+
def list_stored_objects(ctx: Context = None) -> Dict[str, Any]:
|
|
230
|
+
"""List all objects currently held in memory for this session."""
|
|
231
|
+
session_id = _get_session_id(ctx)
|
|
232
|
+
return {"session_id": session_id, "objects": store.list_objects(session_id=session_id)}
|
|
233
|
+
|
|
234
|
+
def inspect_stored_object(obj_id: str, ctx: Context = None) -> Dict[str, Any]:
|
|
235
|
+
"""Inspect detailed attributes, shape, and representation of a stored object."""
|
|
236
|
+
session_id = _get_session_id(ctx)
|
|
237
|
+
if not store.has(obj_id, session_id=session_id):
|
|
238
|
+
return {"error": f"Object '{obj_id}' not found in session '{session_id}'."}
|
|
239
|
+
obj = store.get(obj_id, session_id=session_id)
|
|
240
|
+
attrs = [a for a in dir(obj) if not a.startswith("_")][:25]
|
|
241
|
+
return {
|
|
242
|
+
"object_id": obj_id,
|
|
243
|
+
"type": type(obj).__name__,
|
|
244
|
+
"repr": repr(obj)[:500],
|
|
245
|
+
"shape": getattr(obj, "shape", None),
|
|
246
|
+
"public_attributes": attrs,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
self.mcp.tool(list_stored_objects)
|
|
250
|
+
self.mcp.tool(inspect_stored_object)
|
|
251
|
+
self._registered_tool_names.add("list_stored_objects")
|
|
252
|
+
self._registered_tool_names.add("inspect_stored_object")
|
|
253
|
+
|
|
254
|
+
def expose_function(self, func: Callable[..., Any], name: Optional[str] = None) -> str:
|
|
255
|
+
"""Expose a standalone function as an MCP tool."""
|
|
256
|
+
info = inspect_function(func, name=name)
|
|
257
|
+
tool_name = info.name
|
|
258
|
+
if tool_name in self._registered_tool_names:
|
|
259
|
+
return tool_name
|
|
260
|
+
|
|
261
|
+
doc = format_function_description(info.docstring, is_constructor=False, is_method=False, function_name=tool_name)
|
|
262
|
+
param_code, _, var_pos_name = _build_parameter_list(info.signature)
|
|
263
|
+
|
|
264
|
+
store = self.store
|
|
265
|
+
target_fn = info.target
|
|
266
|
+
|
|
267
|
+
def handler(kwargs: Dict[str, Any], ctx: Optional[Context]) -> Any:
|
|
268
|
+
session_id = _get_session_id(ctx)
|
|
269
|
+
pos_args = []
|
|
270
|
+
if var_pos_name and var_pos_name in kwargs and kwargs[var_pos_name] is not None:
|
|
271
|
+
val = kwargs.pop(var_pos_name)
|
|
272
|
+
if isinstance(val, (list, tuple)):
|
|
273
|
+
pos_args.extend([store.resolve(x, session_id=session_id) for x in val])
|
|
274
|
+
else:
|
|
275
|
+
pos_args.append(store.resolve(val, session_id=session_id))
|
|
276
|
+
# Fallback if caller passed X and y as named arguments
|
|
277
|
+
if not pos_args and "X" in kwargs and "y" in kwargs:
|
|
278
|
+
x_val = kwargs.pop("X", None)
|
|
279
|
+
y_val = kwargs.pop("y", None)
|
|
280
|
+
if x_val is not None or y_val is not None:
|
|
281
|
+
pos_args.extend([
|
|
282
|
+
store.resolve(x_val, session_id=session_id),
|
|
283
|
+
store.resolve(y_val, session_id=session_id),
|
|
284
|
+
])
|
|
285
|
+
kwargs.pop("X", None)
|
|
286
|
+
kwargs.pop("y", None)
|
|
287
|
+
|
|
288
|
+
resolved_kwargs = {
|
|
289
|
+
k: store.resolve(v, session_id=session_id)
|
|
290
|
+
for k, v in kwargs.items()
|
|
291
|
+
if v is not None or k in info.signature.parameters
|
|
292
|
+
}
|
|
293
|
+
try:
|
|
294
|
+
res = target_fn(*pos_args, **resolved_kwargs)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
return {"error": f"Error executing {tool_name}: {e}"}
|
|
297
|
+
return _format_result(res, store, session_id, label=tool_name)
|
|
298
|
+
|
|
299
|
+
wrapper = _create_dynamic_wrapper(tool_name, doc, param_code, handler)
|
|
300
|
+
self.mcp.tool(wrapper)
|
|
301
|
+
self._registered_tool_names.add(tool_name)
|
|
302
|
+
logger.info(f"Registered function tool: {tool_name}")
|
|
303
|
+
return tool_name
|
|
304
|
+
|
|
305
|
+
def expose_class(
|
|
306
|
+
self,
|
|
307
|
+
cls: Type[Any],
|
|
308
|
+
name: Optional[str] = None,
|
|
309
|
+
member_filter: Optional[MemberFilter] = None,
|
|
310
|
+
) -> List[str]:
|
|
311
|
+
"""Expose a class constructor and its public methods as MCP tools."""
|
|
312
|
+
info = inspect_class(cls, name=name, member_filter=member_filter)
|
|
313
|
+
cls_name = info.name
|
|
314
|
+
registered = []
|
|
315
|
+
store = self.store
|
|
316
|
+
|
|
317
|
+
# 1. Constructor tool: <ClassName>_create
|
|
318
|
+
ctor_tool_name = f"{cls_name}_create"
|
|
319
|
+
if ctor_tool_name not in self._registered_tool_names:
|
|
320
|
+
ctor_doc = format_function_description(
|
|
321
|
+
info.docstring,
|
|
322
|
+
is_constructor=True,
|
|
323
|
+
class_name=cls_name,
|
|
324
|
+
)
|
|
325
|
+
param_code, _, _ = _build_parameter_list(info.init_signature, skip_first=True)
|
|
326
|
+
|
|
327
|
+
def ctor_handler(kwargs: Dict[str, Any], ctx: Optional[Context]) -> Any:
|
|
328
|
+
session_id = _get_session_id(ctx)
|
|
329
|
+
resolved_kwargs = {k: store.resolve(v, session_id=session_id) for k, v in kwargs.items()}
|
|
330
|
+
try:
|
|
331
|
+
instance = cls(**resolved_kwargs)
|
|
332
|
+
except Exception as e:
|
|
333
|
+
return {"error": f"Error instantiating {cls_name}: {e}"}
|
|
334
|
+
obj_id = store.register(instance, label=cls_name, session_id=session_id)
|
|
335
|
+
return {
|
|
336
|
+
"status": "success",
|
|
337
|
+
"object_id": obj_id,
|
|
338
|
+
"class": cls_name,
|
|
339
|
+
"message": (
|
|
340
|
+
f"Created {cls_name} instance with object_id '{obj_id}'. "
|
|
341
|
+
f"Use obj_id='{obj_id}' in subsequent method calls like {cls_name}_fit."
|
|
342
|
+
),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
wrapper = _create_dynamic_wrapper(ctor_tool_name, ctor_doc, param_code, ctor_handler)
|
|
346
|
+
self.mcp.tool(wrapper)
|
|
347
|
+
self._registered_tool_names.add(ctor_tool_name)
|
|
348
|
+
registered.append(ctor_tool_name)
|
|
349
|
+
logger.info(f"Registered constructor tool: {ctor_tool_name}")
|
|
350
|
+
|
|
351
|
+
# 2. Methods: <ClassName>_<method_name>
|
|
352
|
+
for method_name, method_info in info.methods.items():
|
|
353
|
+
method_tool_name = f"{cls_name}_{method_name}"
|
|
354
|
+
if method_tool_name in self._registered_tool_names:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
method_doc = format_function_description(
|
|
358
|
+
method_info.docstring,
|
|
359
|
+
is_method=True,
|
|
360
|
+
class_name=cls_name,
|
|
361
|
+
)
|
|
362
|
+
# Add virtual parameter 'obj_id'
|
|
363
|
+
param_code, _, _ = _build_parameter_list(
|
|
364
|
+
method_info.signature,
|
|
365
|
+
skip_first=True,
|
|
366
|
+
extra_lead_params=[("obj_id", "str")],
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Closure capture for method dispatch
|
|
370
|
+
def make_method_handler(m_name: str) -> Callable[[Dict[str, Any], Optional[Context]], Any]:
|
|
371
|
+
def m_handler(kwargs: Dict[str, Any], ctx: Optional[Context]) -> Any:
|
|
372
|
+
session_id = _get_session_id(ctx)
|
|
373
|
+
obj_id = kwargs.pop("obj_id", None)
|
|
374
|
+
if not obj_id or not store.has(obj_id, session_id=session_id):
|
|
375
|
+
return {
|
|
376
|
+
"error": (
|
|
377
|
+
f"Object ID '{obj_id}' not found in session '{session_id}'. "
|
|
378
|
+
f"Available objects: {list(store.list_objects(session_id=session_id).keys())}."
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
instance = store.get(obj_id, session_id=session_id)
|
|
382
|
+
resolved_kwargs = {k: store.resolve(v, session_id=session_id) for k, v in kwargs.items()}
|
|
383
|
+
try:
|
|
384
|
+
bound_method = getattr(instance, m_name)
|
|
385
|
+
res = bound_method(**resolved_kwargs)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
return {"error": f"Error executing {cls_name}.{m_name} on {obj_id}: {e}"}
|
|
388
|
+
return _format_result(
|
|
389
|
+
res,
|
|
390
|
+
store,
|
|
391
|
+
session_id,
|
|
392
|
+
label=f"{cls_name}_{m_name}",
|
|
393
|
+
target_obj_id=obj_id,
|
|
394
|
+
)
|
|
395
|
+
return m_handler
|
|
396
|
+
|
|
397
|
+
wrapper = _create_dynamic_wrapper(
|
|
398
|
+
method_tool_name,
|
|
399
|
+
method_doc,
|
|
400
|
+
param_code,
|
|
401
|
+
make_method_handler(method_name),
|
|
402
|
+
)
|
|
403
|
+
self.mcp.tool(wrapper)
|
|
404
|
+
self._registered_tool_names.add(method_tool_name)
|
|
405
|
+
registered.append(method_tool_name)
|
|
406
|
+
logger.info(f"Registered method tool: {method_tool_name}")
|
|
407
|
+
|
|
408
|
+
return registered
|
|
409
|
+
|
|
410
|
+
def expose_module(
|
|
411
|
+
self,
|
|
412
|
+
module: Any,
|
|
413
|
+
member_filter: Optional[MemberFilter] = None,
|
|
414
|
+
) -> List[str]:
|
|
415
|
+
"""Expose all matching functions and classes from a module."""
|
|
416
|
+
mod_info = inspect_module(module, member_filter=member_filter)
|
|
417
|
+
registered: List[str] = []
|
|
418
|
+
|
|
419
|
+
for fn_name, fn_info in mod_info.functions.items():
|
|
420
|
+
tool_name = self.expose_function(fn_info.target, name=fn_name)
|
|
421
|
+
registered.append(tool_name)
|
|
422
|
+
|
|
423
|
+
for cls_name, cls_info in mod_info.classes.items():
|
|
424
|
+
tools = self.expose_class(cls_info.cls, name=cls_name, member_filter=member_filter)
|
|
425
|
+
registered.extend(tools)
|
|
426
|
+
|
|
427
|
+
return registered
|
|
428
|
+
|
|
429
|
+
def expose(
|
|
430
|
+
self,
|
|
431
|
+
targets: Union[str, List[str], Any],
|
|
432
|
+
include: Optional[Union[str, List[str]]] = None,
|
|
433
|
+
exclude: Optional[Union[str, List[str]]] = None,
|
|
434
|
+
) -> List[str]:
|
|
435
|
+
"""Expose one or more modules, classes, or functions.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
targets: Target string(s) (e.g. 'sklearn.datasets.load_iris') or module/class objects.
|
|
439
|
+
include: Optional regex pattern(s) to include.
|
|
440
|
+
exclude: Optional regex pattern(s) to exclude.
|
|
441
|
+
"""
|
|
442
|
+
member_filter = MemberFilter(include=include, exclude=exclude) if (include or exclude) else None
|
|
443
|
+
|
|
444
|
+
if not isinstance(targets, list):
|
|
445
|
+
targets = [targets]
|
|
446
|
+
|
|
447
|
+
registered: List[str] = []
|
|
448
|
+
for target in targets:
|
|
449
|
+
obj = import_target(target)
|
|
450
|
+
if inspect.isclass(obj):
|
|
451
|
+
registered.extend(self.expose_class(obj, member_filter=member_filter))
|
|
452
|
+
elif inspect.isfunction(obj) or inspect.isbuiltin(obj):
|
|
453
|
+
name = getattr(obj, "__name__", None)
|
|
454
|
+
registered.append(self.expose_function(obj, name=name))
|
|
455
|
+
elif inspect.ismodule(obj):
|
|
456
|
+
registered.extend(self.expose_module(obj, member_filter=member_filter))
|
|
457
|
+
else:
|
|
458
|
+
# Any other callable
|
|
459
|
+
if callable(obj):
|
|
460
|
+
registered.append(self.expose_function(obj))
|
|
461
|
+
else:
|
|
462
|
+
raise TypeError(f"Target '{target}' is not a module, class, or callable.")
|
|
463
|
+
|
|
464
|
+
return registered
|
|
465
|
+
|
|
466
|
+
def run(
|
|
467
|
+
self,
|
|
468
|
+
transport: str = "stdio",
|
|
469
|
+
host: str = "127.0.0.1",
|
|
470
|
+
port: int = 8000,
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Run the MCP server using FastMCP."""
|
|
473
|
+
transport_normalized = transport.lower()
|
|
474
|
+
if transport_normalized in ("http", "streamable-http"):
|
|
475
|
+
self.mcp.run(transport="http", host=host, port=port)
|
|
476
|
+
elif transport_normalized == "sse":
|
|
477
|
+
self.mcp.run(transport="sse", host=host, port=port)
|
|
478
|
+
else:
|
|
479
|
+
self.mcp.run(transport="stdio")
|
integrum/store.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""State management and token-saving word-based object store for Integrum."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import random
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ADJECTIVES: List[str] = [
|
|
9
|
+
"playful", "swift", "calm", "clever", "brave", "bright", "gentle", "silent",
|
|
10
|
+
"golden", "cosmic", "frosty", "amber", "emerald", "lively", "agile", "mighty",
|
|
11
|
+
"nimble", "vibrant", "serene", "dapper", "sunny", "quiet", "daring", "eager",
|
|
12
|
+
"fuzzy", "grand", "happy", "jolly", "keen", "lucid", "merry", "noble",
|
|
13
|
+
"polar", "quick", "radiant", "stellar", "tidy", "unique", "valiant", "warm",
|
|
14
|
+
"witty", "zen", "bold", "crisp", "dawn", "fleet", "gleaming", "honest",
|
|
15
|
+
"iron", "jade", "kind", "lunar", "mystic", "nova", "optic", "proud",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
NOUNS: List[str] = [
|
|
19
|
+
"llama", "falcon", "otter", "badger", "dolphin", "fox", "panda", "tiger",
|
|
20
|
+
"eagle", "sparrow", "wolf", "lynx", "bear", "hawk", "koala", "bison",
|
|
21
|
+
"raven", "crane", "beaver", "puma", "finch", "jaguar", "lemur", "moose",
|
|
22
|
+
"newt", "owl", "quail", "robin", "seal", "swan", "viper", "walrus",
|
|
23
|
+
"yak", "zebra", "comet", "river", "cedar", "coral", "ember", "glacier",
|
|
24
|
+
"haven", "island", "meadow", "oasis", "pebble", "ridge", "shadow", "valley",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WordIdGenerator:
|
|
29
|
+
"""Generates memorable, token-efficient word IDs like 'playful_llama'."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, seed: Optional[int] = None) -> None:
|
|
32
|
+
self._rng = random.Random(seed)
|
|
33
|
+
self._used_ids: set[str] = set()
|
|
34
|
+
|
|
35
|
+
def generate(self, prefix: Optional[str] = None) -> str:
|
|
36
|
+
"""Generate a unique word ID.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
prefix: Optional hint prefix, though word pairs are preferred to save tokens.
|
|
40
|
+
"""
|
|
41
|
+
for _ in range(500):
|
|
42
|
+
adj = self._rng.choice(ADJECTIVES)
|
|
43
|
+
noun = self._rng.choice(NOUNS)
|
|
44
|
+
word_id = f"{adj}_{noun}"
|
|
45
|
+
if word_id not in self._used_ids:
|
|
46
|
+
self._used_ids.add(word_id)
|
|
47
|
+
return word_id
|
|
48
|
+
|
|
49
|
+
# Fallback if dictionary space is heavily populated
|
|
50
|
+
counter = 1
|
|
51
|
+
while True:
|
|
52
|
+
adj = self._rng.choice(ADJECTIVES)
|
|
53
|
+
noun = self._rng.choice(NOUNS)
|
|
54
|
+
word_id = f"{adj}_{noun}_{counter}"
|
|
55
|
+
if word_id not in self._used_ids:
|
|
56
|
+
self._used_ids.add(word_id)
|
|
57
|
+
return word_id
|
|
58
|
+
counter += 1
|
|
59
|
+
|
|
60
|
+
def release(self, word_id: str) -> None:
|
|
61
|
+
"""Release a word ID back to the generator."""
|
|
62
|
+
self._used_ids.discard(word_id)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ObjectStore:
|
|
66
|
+
"""In-memory multi-session store for Python objects with dot-notation resolution."""
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
# session_id -> {obj_id -> python_object}
|
|
70
|
+
self._sessions: Dict[str, Dict[str, Any]] = {}
|
|
71
|
+
# session_id -> {obj_id -> metadata_dict}
|
|
72
|
+
self._metadata: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
73
|
+
self._id_generators: Dict[str, WordIdGenerator] = {}
|
|
74
|
+
|
|
75
|
+
def _get_generator(self, session_id: str) -> WordIdGenerator:
|
|
76
|
+
if session_id not in self._id_generators:
|
|
77
|
+
self._id_generators[session_id] = WordIdGenerator()
|
|
78
|
+
return self._id_generators[session_id]
|
|
79
|
+
|
|
80
|
+
def _ensure_session(self, session_id: str) -> None:
|
|
81
|
+
if session_id not in self._sessions:
|
|
82
|
+
self._sessions[session_id] = {}
|
|
83
|
+
self._metadata[session_id] = {}
|
|
84
|
+
|
|
85
|
+
def register(self, obj: Any, label: Optional[str] = None, session_id: str = "default") -> str:
|
|
86
|
+
"""Register an object into the store and return its word ID.
|
|
87
|
+
|
|
88
|
+
Also generates child references for composite objects (e.g. tuples or bunches).
|
|
89
|
+
"""
|
|
90
|
+
self._ensure_session(session_id)
|
|
91
|
+
generator = self._get_generator(session_id)
|
|
92
|
+
obj_id = generator.generate()
|
|
93
|
+
|
|
94
|
+
self._sessions[session_id][obj_id] = obj
|
|
95
|
+
|
|
96
|
+
# Metadata summary
|
|
97
|
+
type_name = type(obj).__name__
|
|
98
|
+
module_name = getattr(type(obj), "__module__", "")
|
|
99
|
+
full_type = f"{module_name}.{type_name}" if module_name else type_name
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
shape = getattr(obj, "shape", None)
|
|
103
|
+
if shape is not None:
|
|
104
|
+
shape_str = str(shape)
|
|
105
|
+
elif hasattr(obj, "__len__") and not isinstance(obj, (str, bytes)):
|
|
106
|
+
shape_str = f"len={len(obj)}"
|
|
107
|
+
else:
|
|
108
|
+
shape_str = None
|
|
109
|
+
except Exception:
|
|
110
|
+
shape_str = None
|
|
111
|
+
|
|
112
|
+
meta: Dict[str, Any] = {
|
|
113
|
+
"type": full_type,
|
|
114
|
+
"label": label or type_name,
|
|
115
|
+
"shape": shape_str,
|
|
116
|
+
"repr": repr(obj)[:120],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# If object is a tuple/list, register indexed shortcuts
|
|
120
|
+
if isinstance(obj, (tuple, list)):
|
|
121
|
+
item_ids = []
|
|
122
|
+
for idx, item in enumerate(obj):
|
|
123
|
+
child_id = f"{obj_id}_{idx}"
|
|
124
|
+
self._sessions[session_id][child_id] = item
|
|
125
|
+
item_ids.append(child_id)
|
|
126
|
+
meta["items"] = item_ids
|
|
127
|
+
|
|
128
|
+
# If object has keys/attributes (e.g. scikit-learn Bunch or dict), record available sub-paths
|
|
129
|
+
if isinstance(obj, dict) or hasattr(obj, "keys"):
|
|
130
|
+
try:
|
|
131
|
+
keys = list(obj.keys())[:10]
|
|
132
|
+
meta["keys"] = keys
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
self._metadata[session_id][obj_id] = meta
|
|
137
|
+
return obj_id
|
|
138
|
+
|
|
139
|
+
def has(self, key: str, session_id: str = "default") -> bool:
|
|
140
|
+
"""Check if an object or valid dot-notation path exists."""
|
|
141
|
+
self._ensure_session(session_id)
|
|
142
|
+
if key in self._sessions[session_id]:
|
|
143
|
+
return True
|
|
144
|
+
try:
|
|
145
|
+
self.get(key, session_id=session_id)
|
|
146
|
+
return True
|
|
147
|
+
except KeyError:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def get(self, key: str, session_id: str = "default") -> Any:
|
|
151
|
+
"""Retrieve an object by key, supporting dot-notation attribute and index access."""
|
|
152
|
+
self._ensure_session(session_id)
|
|
153
|
+
session_store = self._sessions[session_id]
|
|
154
|
+
|
|
155
|
+
if key in session_store:
|
|
156
|
+
return session_store[key]
|
|
157
|
+
|
|
158
|
+
# Check dot notation e.g. 'playful_llama.data' or index access 'playful_llama[0]'
|
|
159
|
+
if "." in key:
|
|
160
|
+
parts = key.split(".")
|
|
161
|
+
root_id = parts[0]
|
|
162
|
+
if root_id in session_store:
|
|
163
|
+
curr = session_store[root_id]
|
|
164
|
+
for part in parts[1:]:
|
|
165
|
+
if isinstance(curr, dict) and part in curr:
|
|
166
|
+
curr = curr[part]
|
|
167
|
+
elif hasattr(curr, part):
|
|
168
|
+
curr = getattr(curr, part)
|
|
169
|
+
else:
|
|
170
|
+
raise KeyError(f"Attribute or key '{part}' not found on object '{root_id}'")
|
|
171
|
+
return curr
|
|
172
|
+
|
|
173
|
+
# Check bracket indexing e.g. 'playful_llama[0]'
|
|
174
|
+
if "[" in key and key.endswith("]"):
|
|
175
|
+
root_id, index_str = key[:-1].split("[", 1)
|
|
176
|
+
if root_id in session_store:
|
|
177
|
+
curr = session_store[root_id]
|
|
178
|
+
try:
|
|
179
|
+
idx = int(index_str)
|
|
180
|
+
return curr[idx]
|
|
181
|
+
except (ValueError, IndexError, TypeError) as e:
|
|
182
|
+
raise KeyError(f"Index [{index_str}] invalid on object '{root_id}': {e}")
|
|
183
|
+
|
|
184
|
+
available = list(session_store.keys())
|
|
185
|
+
raise KeyError(
|
|
186
|
+
f"Object '{key}' not found in session '{session_id}'. "
|
|
187
|
+
f"Available objects: {available}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def resolve(self, val: Any, session_id: str = "default") -> Any:
|
|
191
|
+
"""Resolve a value to an underlying stored object if it references one."""
|
|
192
|
+
if isinstance(val, str):
|
|
193
|
+
# Strip whitespace
|
|
194
|
+
stripped = val.strip()
|
|
195
|
+
# If it's a registered ID or dot-notation path, resolve it
|
|
196
|
+
if self.has(stripped, session_id=session_id):
|
|
197
|
+
return self.get(stripped, session_id=session_id)
|
|
198
|
+
return val
|
|
199
|
+
|
|
200
|
+
def list_objects(self, session_id: str = "default") -> Dict[str, Dict[str, Any]]:
|
|
201
|
+
"""List summary of all primary objects stored in this session."""
|
|
202
|
+
self._ensure_session(session_id)
|
|
203
|
+
return dict(self._metadata[session_id])
|
|
204
|
+
|
|
205
|
+
def delete(self, key: str, session_id: str = "default") -> bool:
|
|
206
|
+
"""Delete an object from this session."""
|
|
207
|
+
self._ensure_session(session_id)
|
|
208
|
+
if key in self._sessions[session_id]:
|
|
209
|
+
del self._sessions[session_id][key]
|
|
210
|
+
self._metadata[session_id].pop(key, None)
|
|
211
|
+
self._get_generator(session_id).release(key)
|
|
212
|
+
return True
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
def clear(self, session_id: Optional[str] = None) -> None:
|
|
216
|
+
"""Clear objects for a specific session, or all sessions if None."""
|
|
217
|
+
if session_id is not None:
|
|
218
|
+
self._sessions.pop(session_id, None)
|
|
219
|
+
self._metadata.pop(session_id, None)
|
|
220
|
+
self._id_generators.pop(session_id, None)
|
|
221
|
+
else:
|
|
222
|
+
self._sessions.clear()
|
|
223
|
+
self._metadata.clear()
|
|
224
|
+
self._id_generators.clear()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: integrum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert any Python module or library into an MCP server with FastMCP and stateful word-based object management.
|
|
5
|
+
Author: Integrum Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: fastmcp,llm,mcp,reflection,scikit-learn,tools
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: click>=8.1.0
|
|
18
|
+
Requires-Dist: docstring-parser>=0.16
|
|
19
|
+
Requires-Dist: fastmcp>=4.0.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: openai>=1.30.0; extra == 'test'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == 'test'
|
|
26
|
+
Requires-Dist: scikit-learn>=1.4.0; extra == 'test'
|
|
27
|
+
Requires-Dist: tenacity>=8.2.0; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="src/integrum/logo.png" alt="Integrum Logo" width="180"/>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<h1 align="center">Integrum</h1>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<em>Convert any Python module or library into a FastMCP Model Context Protocol server.</em>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
**Integrum** turns any Python module, library, function, or class into a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server powered by [FastMCP](https://github.com/jlowin/fastmcp).
|
|
41
|
+
|
|
42
|
+
Integrum uses Python reflection (`inspect`), regex filtering, and an in-memory **Word-Based Object Store** (`playful_llama`, `swift_falcon`) so that object-oriented libraries (such as `scikit-learn`, `pandas`, `PyTorch`) can be used naturally by LLMs across multi-turn conversations without token bloat.
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Module Reflection**: Automatically introspects functions and classes using `inspect`.
|
|
47
|
+
- **Regex Filtering**: Expose only what you need with `--include` and `--exclude` regex patterns.
|
|
48
|
+
- **Word-Based State Management**: Object IDs are short, memorable words (e.g. `playful_llama`) to save tokens and avoid UUID hallucination.
|
|
49
|
+
- **Multi-User Isolation**: Supports FastMCP `Context` sessions to partition objects per connecting client.
|
|
50
|
+
- **Docstring Instruction Injection**: Transforms Python docstrings into LLM tool documentation and automatically injects state-handling instructions.
|
|
51
|
+
- **Dot-Notation Attribute Resolution**: Pass `swift_falcon.data` or `swift_falcon.target` directly to subsequent tool calls like `fit()`.
|
|
52
|
+
- **CLI & Python API**: Run from the command line (`integrum expose`) or integrate directly into Python scripts.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv pip install integrum
|
|
58
|
+
# or
|
|
59
|
+
pip install integrum
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start (CLI)
|
|
63
|
+
|
|
64
|
+
Expose `scikit-learn` dataset loaders and RandomForestClassifier as an MCP server:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
integrum expose sklearn.datasets.load_iris sklearn.ensemble.RandomForestClassifier
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Preview what tools will be exposed:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
integrum list sklearn.datasets.load_iris sklearn.ensemble.RandomForestClassifier
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Filter functions and methods with regex:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
integrum expose sklearn.datasets sklearn.ensemble \
|
|
80
|
+
--include "^(load_iris|RandomForestClassifier)$"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run with HTTP or SSE transport:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
integrum expose sklearn.datasets.load_iris --transport http --port 8000
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Python API
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from integrum import IntegrumServer
|
|
93
|
+
|
|
94
|
+
server = IntegrumServer(name="ml-server")
|
|
95
|
+
server.expose(["sklearn.datasets.load_iris", "sklearn.ensemble.RandomForestClassifier"])
|
|
96
|
+
|
|
97
|
+
# Run over stdio (default for MCP desktop clients)
|
|
98
|
+
server.run(transport="stdio")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## How State Management Works
|
|
102
|
+
|
|
103
|
+
When a class is exposed, Integrum registers:
|
|
104
|
+
1. `<ClassName>_create`: Instantiates the class and stores it under a word ID (e.g., `playful_llama`).
|
|
105
|
+
2. `<ClassName>_<method>`: Accepts a virtual parameter `obj_id: str` (e.g., `obj_id="playful_llama"`), resolves the underlying instance, and executes the method.
|
|
106
|
+
|
|
107
|
+
Returned objects (such as datasets or numpy arrays) are also registered in the store with word IDs, allowing subsequent tools to receive them as arguments.
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
Check out [`examples/gemma_sklearn_session.md`](examples/gemma_sklearn_session.md) and [`examples/gemma_sklearn_session.json`](examples/gemma_sklearn_session.json) for a complete, verified multi-turn transcript of Gemma 4 26B loading the Iris dataset, performing `train_test_split`, training a `RandomForestClassifier`, and evaluating accuracy.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
integrum/__init__.py,sha256=HDKO5ci3Vwn3S6Z2FF67P99lj3TO3IjaOLY7vK2KdyQ,515
|
|
2
|
+
integrum/cli.py,sha256=I44Nj6qiAL71uqCHmV16afUwr-IdNZrhMyl9VN_0piA,4571
|
|
3
|
+
integrum/docstrings.py,sha256=gup2MEknEJKDby1Wi7bdIbJhsTZYiOrDhOuIkgrwqMY,4326
|
|
4
|
+
integrum/filter.py,sha256=IZHbMsSIR6uQCcaFbbw7-j5n_dut4fZT88XKsJxwlWE,2024
|
|
5
|
+
integrum/inspector.py,sha256=GpunOwOGzN8Ohau6I5kmhny8kBU8ozBZidrgJ0nUW_Y,6135
|
|
6
|
+
integrum/logo.png,sha256=uFMU3N6dYFXs-c2Aew91K9dfOP_CRYtIy-xVCMUbxsg,49983
|
|
7
|
+
integrum/logo_white.png,sha256=WZH2fVwMzdlBC4ay3w5ZI6eL6D81Su66t9YeJdj7XuA,49819
|
|
8
|
+
integrum/server.py,sha256=m22zboMaS88VGcZdcElbMrtRUsBW9u9MHf8Jz_VWu5U,19455
|
|
9
|
+
integrum/store.py,sha256=CnmlQ_IrJM6D9nTaOcG5W2ox-QOrrZ6YL3ihBipTnu0,8949
|
|
10
|
+
integrum-0.1.0.dist-info/METADATA,sha256=uQbOqOvIjJ609Qga-8uC2cwhy6ZmKtJdQwRFh3Cre5k,4624
|
|
11
|
+
integrum-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
12
|
+
integrum-0.1.0.dist-info/entry_points.txt,sha256=NeG6TVeLe0jYuEtbWGeRN_ngb2g5-rtiTNqwr-4P4dE,47
|
|
13
|
+
integrum-0.1.0.dist-info/RECORD,,
|