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 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
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ integrum = integrum.cli:main