synapse-framework 0.2.4__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.
Files changed (54) hide show
  1. synapse/__init__.py +207 -0
  2. synapse/app.py +942 -0
  3. synapse/cli.py +88 -0
  4. synapse/config.py +103 -0
  5. synapse/connectors/__init__.py +29 -0
  6. synapse/connectors/base.py +222 -0
  7. synapse/connectors/factory.py +202 -0
  8. synapse/connectors/gmail.py +292 -0
  9. synapse/connectors/mcp_wrapper.py +153 -0
  10. synapse/connectors/notion.py +161 -0
  11. synapse/connectors/photos.py +141 -0
  12. synapse/database/__init__.py +19 -0
  13. synapse/database/base.py +63 -0
  14. synapse/database/chroma.py +175 -0
  15. synapse/database/memory.py +132 -0
  16. synapse/database/pgvector.py +99 -0
  17. synapse/database/qdrant.py +138 -0
  18. synapse/embeddings/__init__.py +21 -0
  19. synapse/embeddings/base.py +262 -0
  20. synapse/export/__init__.py +14 -0
  21. synapse/export/mcp_exporter.py +202 -0
  22. synapse/graph/__init__.py +21 -0
  23. synapse/graph/base.py +130 -0
  24. synapse/graph/memory.py +20 -0
  25. synapse/graph/neo4j_store.py +176 -0
  26. synapse/graph/networkx_store.py +221 -0
  27. synapse/inspector/__init__.py +5 -0
  28. synapse/inspector/template.py +530 -0
  29. synapse/models/__init__.py +73 -0
  30. synapse/models/base.py +95 -0
  31. synapse/models/gemini.py +328 -0
  32. synapse/models/ollama.py +322 -0
  33. synapse/models/openai.py +289 -0
  34. synapse/oauth/__init__.py +7 -0
  35. synapse/oauth/google.py +194 -0
  36. synapse/oauth/notion.py +35 -0
  37. synapse/rag/__init__.py +8 -0
  38. synapse/rag/chunker.py +219 -0
  39. synapse/rag/hybrid_rag.py +122 -0
  40. synapse/resource/__init__.py +5 -0
  41. synapse/resource/resource.py +304 -0
  42. synapse/resources/__init__.py +5 -0
  43. synapse/skills/__init__.py +5 -0
  44. synapse/skills/skill.py +321 -0
  45. synapse/storage/__init__.py +24 -0
  46. synapse/storage/graph_exporter.py +624 -0
  47. synapse/storage/graph_store.py +162 -0
  48. synapse/storage/vector_store.py +103 -0
  49. synapse_framework-0.2.4.dist-info/METADATA +415 -0
  50. synapse_framework-0.2.4.dist-info/RECORD +54 -0
  51. synapse_framework-0.2.4.dist-info/WHEEL +5 -0
  52. synapse_framework-0.2.4.dist-info/entry_points.txt +2 -0
  53. synapse_framework-0.2.4.dist-info/licenses/LICENSE +21 -0
  54. synapse_framework-0.2.4.dist-info/top_level.txt +1 -0
synapse/cli.py ADDED
@@ -0,0 +1,88 @@
1
+ """Command-line interface for running the Synapse AI inspector server and MCP stdio engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from synapse.app import BaseApp
9
+ from synapse.connectors.gmail import GmailConnector
10
+ from synapse.connectors.notion import NotionConnector
11
+ from synapse.export.mcp_exporter import MCPExporter
12
+ from synapse.models.ollama import OllamaModel
13
+
14
+
15
+ def build_default_app(data_dir: str = "data") -> BaseApp:
16
+ """Constructs the default CLI Synapse application instance."""
17
+ return BaseApp(
18
+ title="Synapse AI",
19
+ data_dir=Path(data_dir),
20
+ model=OllamaModel(),
21
+ connectors=[
22
+ GmailConnector(),
23
+ NotionConnector(),
24
+ ],
25
+ persistent_storage=True,
26
+ Visualize_graph=True,
27
+ )
28
+
29
+
30
+ def main() -> None:
31
+ """CLI entrypoint: `synapse`, `synapse mcp`, or `python -m synapse.cli`."""
32
+ parser = argparse.ArgumentParser(
33
+ description="Synapse — Multi-MCP & Hybrid RAG Framework",
34
+ )
35
+ parser.add_argument(
36
+ "subcommand",
37
+ nargs="?",
38
+ default="serve",
39
+ choices=["serve", "mcp", "export"],
40
+ help="Action to run: 'serve' (web inspector/API), 'mcp' (stdio server for Claude/Cursor), or 'export' (generate config)",
41
+ )
42
+ parser.add_argument(
43
+ "--host",
44
+ default="127.0.0.1",
45
+ help="Host interface to bind (default: 127.0.0.1)",
46
+ )
47
+ parser.add_argument(
48
+ "--port",
49
+ type=int,
50
+ default=8000,
51
+ help="Port number to listen on (default: 8000)",
52
+ )
53
+ parser.add_argument(
54
+ "--reload",
55
+ action="store_true",
56
+ help="Enable uvicorn hot-reloading",
57
+ )
58
+ parser.add_argument(
59
+ "--data-dir",
60
+ default="data",
61
+ help="Storage directory for ChromaDB and NetworkX graph (default: data)",
62
+ )
63
+ parser.add_argument(
64
+ "--mcp",
65
+ action="store_true",
66
+ help="Shortcut to run in stdio MCP mode",
67
+ )
68
+ parser.add_argument(
69
+ "--output",
70
+ default="claude_desktop_config.json",
71
+ help="Destination path for 'export' subcommand",
72
+ )
73
+
74
+ args = parser.parse_args()
75
+ app = build_default_app(data_dir=args.data_dir)
76
+
77
+ if args.mcp or args.subcommand == "mcp":
78
+ # Run stdio MCP server for Claude Desktop / Cursor
79
+ app.run_mcp(transport="stdio")
80
+ elif args.subcommand == "export":
81
+ cfg_path = MCPExporter.save_claude_desktop_config(app, filepath=args.output)
82
+ print(f"✓ Claude Desktop config saved to: {cfg_path}")
83
+ else:
84
+ app.run(host=args.host, port=args.port, reload=args.reload)
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
synapse/config.py ADDED
@@ -0,0 +1,103 @@
1
+ """Configuration constants and design tokens for the Synapse framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ # Base filesystem directories (default to working directory or user-specified)
10
+ DEFAULT_WORKSPACE_DIR: Path = Path(
11
+ os.getenv("SYNAPSE_WORKSPACE_DIR", Path.cwd())
12
+ ).resolve()
13
+ DEFAULT_DATA_DIR: Path = DEFAULT_WORKSPACE_DIR / "data"
14
+ DEFAULT_CHROMA_DIR: Path = DEFAULT_DATA_DIR / "chroma_db"
15
+ DEFAULT_GRAPH_FILE: Path = DEFAULT_DATA_DIR / "graph.json"
16
+
17
+ # Default Model settings
18
+ DEFAULT_OLLAMA_BASE_URL: str = os.getenv(
19
+ "OLLAMA_BASE_URL", "http://localhost:11434"
20
+ )
21
+ DEFAULT_OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "qwen2.5:0.5b")
22
+ DEFAULT_OPENAI_MODEL: str = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
23
+ DEFAULT_GEMINI_MODEL: str = os.getenv("GEMINI_MODEL", "gemini-flash-latest")
24
+ DEFAULT_TEMPERATURE: float = float(os.getenv("SYNAPSE_TEMPERATURE", "0.1"))
25
+
26
+ # Embeddings & Chunking
27
+ DEFAULT_EMBEDDING_MODEL: str = os.getenv(
28
+ "SYNAPSE_EMBEDDING_MODEL", "BAAI/bge-small-en-v1.5"
29
+ )
30
+ DEFAULT_CHUNK_SIZE: int = int(os.getenv("SYNAPSE_CHUNK_SIZE", "280"))
31
+ DEFAULT_CHUNK_OVERLAP: int = int(os.getenv("SYNAPSE_CHUNK_OVERLAP", "30"))
32
+ DEFAULT_CHROMA_COLLECTION: str = "synapse_memory"
33
+ DEFAULT_TOP_K: int = int(os.getenv("SYNAPSE_TOP_K", "3"))
34
+
35
+ # Design Tokens: Solid Matte / Zinc Specification
36
+ DESIGN_TOKENS: dict[str, Any] = {
37
+ "surface": {
38
+ "canvas": "#09090b", # Zinc 950
39
+ "panel": "#18181b", # Zinc 900
40
+ "panel_hover": "#27272a", # Zinc 800
41
+ "inset": "#121215", # Zinc 925
42
+ },
43
+ "border": {
44
+ "subtle": "#27272a", # Zinc 800
45
+ "strong": "#3f3f46", # Zinc 700
46
+ },
47
+ "text": {
48
+ "primary": "#f4f4f5", # Zinc 100
49
+ "secondary": "#a1a1aa", # Zinc 400
50
+ "muted": "#71717a", # Zinc 500
51
+ "font_family": "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
52
+ },
53
+ "palettes": {
54
+ "zinc": {
55
+ "Person": "#818cf8",
56
+ "Email": "#38bdf8",
57
+ "Project": "#34d399",
58
+ "Tool": "#fbbf24",
59
+ "Topic": "#f472b6",
60
+ "Database": "#fb923c",
61
+ "Resource": "#a78bfa",
62
+ "Document": "#60a5fa",
63
+ "Framework": "#2dd4bf",
64
+ "Default": "#94a3b8",
65
+ },
66
+ "stone": {
67
+ "Person": "#e07a5f",
68
+ "Email": "#81b29a",
69
+ "Project": "#f2cc8f",
70
+ "Tool": "#d4a373",
71
+ "Topic": "#b5838d",
72
+ "Database": "#e76f51",
73
+ "Resource": "#6d6875",
74
+ "Document": "#a8dadc",
75
+ "Framework": "#588157",
76
+ "Default": "#a8a29e",
77
+ },
78
+ "basalt": {
79
+ "Person": "#93c5fd",
80
+ "Email": "#67e8f9",
81
+ "Project": "#6ee7b7",
82
+ "Tool": "#fdba74",
83
+ "Topic": "#f9a8d4",
84
+ "Database": "#fca5a5",
85
+ "Resource": "#c4b5fd",
86
+ "Document": "#7dd3fc",
87
+ "Framework": "#14b8a6",
88
+ "Default": "#cbd5e1",
89
+ },
90
+ "neon": {
91
+ "Person": "#00f5d4",
92
+ "Email": "#00bbf9",
93
+ "Project": "#7b2cbf",
94
+ "Tool": "#fee440",
95
+ "Topic": "#f72585",
96
+ "Database": "#ff0054",
97
+ "Resource": "#9b5de5",
98
+ "Document": "#3a86ff",
99
+ "Framework": "#06d6a0",
100
+ "Default": "#00f0ff",
101
+ },
102
+ },
103
+ }
@@ -0,0 +1,29 @@
1
+ """Connector definitions and construction factory for Synapse."""
2
+
3
+ from synapse.connectors.base import BaseConnector
4
+ from synapse.connectors.factory import make_connector, makeconnectors
5
+ from synapse.connectors.gmail import GmailConnector, GmailConnectors
6
+ from synapse.connectors.mcp_wrapper import MCPConnector
7
+ from synapse.connectors.notion import (
8
+ NotionConnector,
9
+ NotionConnectors,
10
+ NotionsConnector,
11
+ )
12
+ from synapse.connectors.photos import (
13
+ GooglePhotosConnector,
14
+ GooglePhotosConnectors,
15
+ )
16
+
17
+ __all__ = [
18
+ "BaseConnector",
19
+ "GmailConnector",
20
+ "GmailConnectors",
21
+ "GooglePhotosConnector",
22
+ "GooglePhotosConnectors",
23
+ "MCPConnector",
24
+ "NotionConnector",
25
+ "NotionConnectors",
26
+ "NotionsConnector",
27
+ "make_connector",
28
+ "makeconnectors",
29
+ ]
@@ -0,0 +1,222 @@
1
+ """Abstract base class for all Synapse connectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any
10
+
11
+ from langchain_core.documents import Document
12
+
13
+
14
+ class BaseConnector(ABC):
15
+ """Abstract interface for all Synapse external connectors.
16
+
17
+ Every connector exposes tools, authentication state checks, intent matching,
18
+ and automatic indexing logic into ChromaDB and NetworkX.
19
+
20
+ Attributes:
21
+ name: Unique logical connector identifier (e.g. 'gmail', 'notion', 'photos').
22
+ description: Human-readable summary of the connector's domain.
23
+ intent_regex: Optional regex string used as a first-stage intent gate.
24
+ default_tool: Default tool invoked during sync or fallback operations.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ name: str,
30
+ description: str,
31
+ intent_regex: str | None = None,
32
+ default_tool: str = "",
33
+ ) -> None:
34
+ """Initializes the base connector.
35
+
36
+ Args:
37
+ name: Connector identifier.
38
+ description: Description of the connector data and capabilities.
39
+ intent_regex: Optional regex string matching user prompts that require this connector.
40
+ default_tool: Default tool name.
41
+ """
42
+ self.name = name
43
+ self.description = description
44
+ self.intent_regex = intent_regex
45
+ self._compiled_regex = (
46
+ re.compile(intent_regex, re.IGNORECASE) if intent_regex else None
47
+ )
48
+ self.default_tool = default_tool
49
+
50
+ def matches_intent(self, user_message: str) -> bool:
51
+ """Checks if a user message matches this connector's intent regex."""
52
+ if not self._compiled_regex:
53
+ return True
54
+ return bool(self._compiled_regex.search(user_message or ""))
55
+
56
+ @abstractmethod
57
+ def is_authenticated(self) -> bool:
58
+ """Checks whether the connector has valid authentication credentials."""
59
+ ...
60
+
61
+ @abstractmethod
62
+ async def list_tools(self) -> list[dict[str, Any]]:
63
+ """Returns the list of tool schemas exposed by this connector."""
64
+ ...
65
+
66
+ @abstractmethod
67
+ async def call_tool(
68
+ self,
69
+ tool_name: str,
70
+ arguments: dict[str, Any] | None = None,
71
+ ) -> Any:
72
+ """Executes a named tool with arguments.
73
+
74
+ Args:
75
+ tool_name: Name of the tool to execute.
76
+ arguments: Tool arguments dictionary.
77
+
78
+ Returns:
79
+ Raw tool execution output (list of records or dictionary).
80
+ """
81
+ ...
82
+
83
+ def default_args_builder(self, user_message: str) -> dict[str, Any]:
84
+ """Builds default tool arguments from user prompt when arguments are missing."""
85
+ return {}
86
+
87
+ def chunk_record(
88
+ self,
89
+ tool_name: str,
90
+ record: dict[str, Any],
91
+ ) -> list[Document]:
92
+ """Standardized chunking normalizer for records produced by this connector.
93
+
94
+ Subclasses may override this method for specialized domain chunking.
95
+ """
96
+ rec_id = str(
97
+ record.get("id")
98
+ or record.get("url")
99
+ or hashlib.sha256(str(record).encode("utf-8")).hexdigest()[:12]
100
+ )
101
+ title = str(
102
+ record.get("title")
103
+ or record.get("subject")
104
+ or record.get("name")
105
+ or f"{self.name.upper()} Record"
106
+ ).strip()
107
+ author = str(
108
+ record.get("sender")
109
+ or record.get("from")
110
+ or record.get("author")
111
+ or self.name
112
+ ).strip()
113
+ date_str = str(
114
+ record.get("date")
115
+ or record.get("created_at")
116
+ or record.get("last_edited_time")
117
+ or ""
118
+ ).strip()
119
+ body = str(
120
+ record.get("body")
121
+ or record.get("content")
122
+ or record.get("text")
123
+ or record.get("snippet")
124
+ or ""
125
+ ).strip()
126
+ url = str(record.get("url") or "").strip()
127
+
128
+ if not body and not title:
129
+ return []
130
+
131
+ header = f"[{self.name.upper()} | {tool_name}] Title: {title} | Source: {author}"
132
+ if url:
133
+ header += f" | URL: {url}"
134
+ full_text = f"{header}\nDate: {date_str}\n\n{body}".strip()
135
+
136
+ base_meta: dict[str, Any] = {
137
+ "source": self.name,
138
+ "tool": tool_name,
139
+ "record_id": rec_id,
140
+ "subject": title,
141
+ "sender": author,
142
+ "date": date_str,
143
+ "url": url,
144
+ "type": str(record.get("type") or self.name.capitalize()),
145
+ "chunk_id": f"{self.name}:{rec_id}:0",
146
+ }
147
+
148
+ return [Document(page_content=full_text, metadata=base_meta)]
149
+
150
+ def format_record(
151
+ self,
152
+ tool_name: str,
153
+ record: dict[str, Any],
154
+ index: int = 1,
155
+ ) -> tuple[str, dict[str, Any], str]:
156
+ """Formats a record for prompt context, UI RAG chunks, and knowledge graph triple.
157
+
158
+ Returns:
159
+ A tuple of (formatted_summary_line, live_chunk_dict, graph_triple_string).
160
+ """
161
+ title = str(
162
+ record.get("title")
163
+ or record.get("subject")
164
+ or record.get("name")
165
+ or record.get("action")
166
+ or f"{self.name.upper()} #{index}"
167
+ )
168
+ author = str(
169
+ record.get("sender")
170
+ or record.get("author")
171
+ or record.get("owner")
172
+ or self.name.capitalize()
173
+ )
174
+ date_str = str(record.get("date") or "")
175
+ body_excerpt = str(
176
+ record.get("body")
177
+ or record.get("content")
178
+ or record.get("snippet")
179
+ or record.get("message")
180
+ or record.get("text")
181
+ or record.get("details")
182
+ or record.get("result")
183
+ or (json.dumps(record, ensure_ascii=False) if record else "")
184
+ )[:600].strip()
185
+ entity_type = str(record.get("type") or "Topic").capitalize()
186
+
187
+ summary_line = (
188
+ f"{index}. [{self.name.upper()}] {title}\n"
189
+ f" Source: {author} | Date: {date_str}\n"
190
+ f" Content: {body_excerpt}"
191
+ )
192
+
193
+ live_chunk = {
194
+ "page_content": (
195
+ f"[{self.name.upper()} | {tool_name}] {title}\n"
196
+ f"Date: {date_str}\n\n{body_excerpt}"
197
+ ),
198
+ "metadata": {
199
+ "source": f"{self.name}_live",
200
+ "subject": title,
201
+ "sender": author,
202
+ "date": date_str,
203
+ "type": entity_type,
204
+ },
205
+ "source": f"{self.name}_live",
206
+ "subject": title,
207
+ "chunk_id": f"live_{self.name}_{record.get('id', index)}",
208
+ }
209
+
210
+ triple = f"{author} -> CONTAINS -> {title}"
211
+ return summary_line, live_chunk, triple
212
+
213
+ def to_mcp_server(self, name: str | None = None) -> Any:
214
+ """Converts this connector into an official MCP Server instance."""
215
+ from synapse.export.mcp_exporter import MCPExporter
216
+ return MCPExporter.create_mcp_server(self, name=name or self.name)
217
+
218
+ def run_mcp(self, transport: str = "stdio") -> None:
219
+ """Runs this connector as a standalone MCP server over stdio or SSE."""
220
+ server = self.to_mcp_server()
221
+ server.run(transport=transport)
222
+
@@ -0,0 +1,202 @@
1
+ """Connector factory functions for dynamically constructing BaseConnector instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from synapse.connectors.base import BaseConnector
11
+ from synapse.connectors.mcp_wrapper import MCPConnector
12
+
13
+
14
+ class FunctionalConnector(BaseConnector):
15
+ """Dynamic BaseConnector implementation created by `make_connector`."""
16
+
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ service_manager: Any = None,
21
+ server: Any = None,
22
+ description: str = "",
23
+ intent_regex: str | None = None,
24
+ tools: dict[str, Callable[..., Any]] | None = None,
25
+ default_tool: str = "",
26
+ is_authenticated_fn: Callable[[], bool] | None = None,
27
+ ) -> None:
28
+ super().__init__(
29
+ name=name,
30
+ description=description or f"{name} custom connector",
31
+ intent_regex=intent_regex,
32
+ default_tool=default_tool,
33
+ )
34
+ self.service_manager = service_manager
35
+ self.server = server
36
+ self.tools_dict: dict[str, Callable[..., Any]] = tools or {}
37
+ self.tools = self.tools_dict
38
+ self.is_authenticated_fn = is_authenticated_fn
39
+
40
+ def is_authenticated(self) -> bool:
41
+ if self.is_authenticated_fn:
42
+ try:
43
+ return bool(self.is_authenticated_fn())
44
+ except Exception:
45
+ return False
46
+ if self.service_manager and hasattr(
47
+ self.service_manager, "is_authenticated"
48
+ ):
49
+ try:
50
+ return bool(self.service_manager.is_authenticated())
51
+ except Exception:
52
+ return False
53
+ return True
54
+
55
+ async def list_tools(self) -> list[dict[str, Any]]:
56
+ schemas: list[dict[str, Any]] = []
57
+ for tool_name, func in self.tools_dict.items():
58
+ doc = inspect.getdoc(func) or f"Execute {tool_name}."
59
+ props: dict[str, Any] = {}
60
+ required: list[str] = []
61
+ try:
62
+ sig = inspect.signature(func)
63
+ for param_name, param in sig.parameters.items():
64
+ if param_name in ("self", "cls"):
65
+ continue
66
+ p_type = "string"
67
+ if param.annotation is int:
68
+ p_type = "integer"
69
+ elif param.annotation is float:
70
+ p_type = "number"
71
+ elif param.annotation is bool:
72
+ p_type = "boolean"
73
+ elif param.annotation is list:
74
+ p_type = "array"
75
+ elif param.annotation is dict:
76
+ p_type = "object"
77
+
78
+ prop_info: dict[str, Any] = {"type": p_type}
79
+ if param.default is inspect.Parameter.empty:
80
+ required.append(param_name)
81
+ else:
82
+ prop_info["default"] = param.default
83
+ props[param_name] = prop_info
84
+ except Exception:
85
+ pass # nosec B110
86
+
87
+ schema_obj: dict[str, Any] = {
88
+ "type": "object",
89
+ "properties": props,
90
+ }
91
+ if required:
92
+ schema_obj["required"] = required
93
+
94
+ schemas.append({
95
+ "server": self.name,
96
+ "name": tool_name,
97
+ "description": doc,
98
+ "inputSchema": schema_obj,
99
+ })
100
+ return schemas
101
+
102
+
103
+ async def call_tool(
104
+ self,
105
+ tool_name: str,
106
+ arguments: dict[str, Any] | None = None,
107
+ ) -> Any:
108
+ if tool_name not in self.tools_dict:
109
+ raise KeyError(
110
+ f"Tool '{tool_name}' not registered on connector '{self.name}'."
111
+ )
112
+ func = self.tools_dict[tool_name]
113
+ args = arguments or {}
114
+ if inspect.iscoroutinefunction(func):
115
+ return await func(**args)
116
+ return func(**args)
117
+
118
+
119
+ def make_connector(
120
+ name: str,
121
+ service_manager: Any = None,
122
+ server: Any = None,
123
+ description: str = "",
124
+ intent_regex: str | None = None,
125
+ tools: list[Callable[..., Any]] | dict[str, Callable[..., Any]] | None = None,
126
+ default_tool: str = "",
127
+ is_authenticated_fn: Callable[[], bool] | None = None,
128
+ ) -> BaseConnector:
129
+ """Creates a Synapse `BaseConnector` from custom service managers or servers.
130
+
131
+ Args:
132
+ name: Unique connector name.
133
+ service_manager: Optional auth/service manager (e.g. GoogleOAuthService, NotionAuthService).
134
+ server: Optional FastMCP server script path, server instance, or Stdio command.
135
+ description: Description of the connector's capabilities.
136
+ intent_regex: Optional regex string matching relevant user messages.
137
+ tools: Optional dictionary or list of callable functions exposed as tools.
138
+ default_tool: Optional default tool identifier.
139
+ is_authenticated_fn: Optional custom readiness check callable.
140
+
141
+ Returns:
142
+ A ready-to-use `BaseConnector` instance.
143
+
144
+ Example:
145
+ ```python
146
+ from synapse.connectors import make_connector
147
+
148
+ def search_docs(query: str = ""):
149
+ return [{"title": "Quickstart", "content": "..."}]
150
+
151
+ docs_connector = make_connector(
152
+ name="docs",
153
+ description="Documentation search",
154
+ tools={"search_docs": search_docs},
155
+ default_tool="search_docs",
156
+ )
157
+ ```
158
+ """
159
+ # If server is a script path or command, wrap via MCPConnector
160
+ if isinstance(server, (str, Path)) and (
161
+ str(server).endswith(".py") or Path(server).exists()
162
+ ):
163
+ return MCPConnector(
164
+ name=name,
165
+ script_path=server,
166
+ description=description,
167
+ intent_regex=intent_regex,
168
+ default_tool=default_tool,
169
+ is_ready_fn=is_authenticated_fn,
170
+ )
171
+
172
+ # Normalize tools into a dictionary
173
+ normalized_tools: dict[str, Callable[..., Any]] = {}
174
+ if isinstance(tools, dict):
175
+ normalized_tools = tools
176
+ elif isinstance(tools, list):
177
+ for fn in tools:
178
+ fn_name = getattr(fn, "__name__", str(fn))
179
+ normalized_tools[fn_name] = fn
180
+
181
+ # Automatically extract tools from server if server has tools registry
182
+ if server and hasattr(server, "_tools"):
183
+ for t_name, t_fn in getattr(server, "_tools", {}).items():
184
+ if t_name not in normalized_tools:
185
+ normalized_tools[t_name] = t_fn
186
+
187
+ chosen_default = default_tool or ""
188
+
189
+ return FunctionalConnector(
190
+ name=name,
191
+ service_manager=service_manager,
192
+ server=server,
193
+ description=description,
194
+ intent_regex=intent_regex,
195
+ tools=normalized_tools,
196
+ default_tool=chosen_default,
197
+ is_authenticated_fn=is_authenticated_fn,
198
+ )
199
+
200
+
201
+ # Developer-friendly alias matching user request
202
+ makeconnectors = make_connector