shotgrid-mcp-server 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shotgrid_mcp_server/__init__.py +3 -0
- shotgrid_mcp_server/__main__.py +7 -0
- shotgrid_mcp_server/connection_pool.py +280 -0
- shotgrid_mcp_server/constants.py +51 -0
- shotgrid_mcp_server/logger.py +91 -0
- shotgrid_mcp_server/mockgun_ext.py +559 -0
- shotgrid_mcp_server/py.typed +1 -0
- shotgrid_mcp_server/server.py +432 -0
- shotgrid_mcp_server/utils.py +212 -0
- shotgrid_mcp_server-0.2.1.dist-info/METADATA +178 -0
- shotgrid_mcp_server-0.2.1.dist-info/RECORD +14 -0
- shotgrid_mcp_server-0.2.1.dist-info/WHEEL +4 -0
- shotgrid_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
- shotgrid_mcp_server-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""ShotGrid connection pool module.
|
|
2
|
+
|
|
3
|
+
This module provides a thread-safe connection pool for ShotGrid API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Import built-in modules
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import Any, Optional, Type
|
|
13
|
+
|
|
14
|
+
# Import third-party modules
|
|
15
|
+
from shotgun_api3 import Shotgun
|
|
16
|
+
|
|
17
|
+
# Import local modules
|
|
18
|
+
from src.shotgrid_mcp_server.mockgun_ext import MockgunExt
|
|
19
|
+
|
|
20
|
+
# Configure logging
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ShotgunClientFactory(ABC):
|
|
25
|
+
"""Abstract factory for creating ShotGrid clients."""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def create_client(self) -> Shotgun:
|
|
29
|
+
"""Create a new ShotGrid client.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Shotgun: A new ShotGrid client instance.
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RealShotgunFactory(ShotgunClientFactory):
|
|
38
|
+
"""Factory for creating real ShotGrid clients."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
url: str,
|
|
43
|
+
script_name: str,
|
|
44
|
+
script_key: str,
|
|
45
|
+
http_proxy: Optional[str] = None,
|
|
46
|
+
ca_certs: Optional[str] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize the factory.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
url: ShotGrid server URL
|
|
52
|
+
script_name: Script name for authentication
|
|
53
|
+
script_key: Script key for authentication
|
|
54
|
+
http_proxy: Optional HTTP proxy
|
|
55
|
+
ca_certs: Optional CA certificates path
|
|
56
|
+
"""
|
|
57
|
+
self.url = url
|
|
58
|
+
self.script_name = script_name
|
|
59
|
+
self.script_key = script_key
|
|
60
|
+
self.http_proxy = http_proxy
|
|
61
|
+
self.ca_certs = ca_certs
|
|
62
|
+
|
|
63
|
+
def create_client(self) -> Shotgun:
|
|
64
|
+
"""Create a real ShotGrid client.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Shotgun: A new ShotGrid client instance.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
Exception: If connection creation fails.
|
|
71
|
+
"""
|
|
72
|
+
sg = Shotgun(
|
|
73
|
+
self.url,
|
|
74
|
+
script_name=self.script_name,
|
|
75
|
+
api_key=self.script_key,
|
|
76
|
+
http_proxy=self.http_proxy,
|
|
77
|
+
ca_certs=self.ca_certs,
|
|
78
|
+
)
|
|
79
|
+
sg.connect()
|
|
80
|
+
logger.info("Successfully connected to ShotGrid at %s", self.url)
|
|
81
|
+
return sg
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MockShotgunFactory(ShotgunClientFactory):
|
|
85
|
+
"""Factory for creating mock ShotGrid clients."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, schema_path: str, schema_entity_path: str) -> None:
|
|
88
|
+
"""Initialize the factory.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
schema_path: Path to schema.json
|
|
92
|
+
schema_entity_path: Path to schema_entity.json
|
|
93
|
+
"""
|
|
94
|
+
self.schema_path = schema_path
|
|
95
|
+
self.schema_entity_path = schema_entity_path
|
|
96
|
+
|
|
97
|
+
def create_client(self) -> MockgunExt:
|
|
98
|
+
"""Create a mock ShotGrid client.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
MockgunExt: A new mock ShotGrid client instance.
|
|
102
|
+
"""
|
|
103
|
+
# Set schema paths before creating the instance
|
|
104
|
+
MockgunExt.set_schema_paths(self.schema_path, self.schema_entity_path)
|
|
105
|
+
sg = MockgunExt(
|
|
106
|
+
"https://test.shotgunstudio.com",
|
|
107
|
+
script_name="test_script",
|
|
108
|
+
api_key="test_key",
|
|
109
|
+
)
|
|
110
|
+
logger.debug("Created mock ShotGrid connection")
|
|
111
|
+
return sg
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ShotGridConnectionPool:
|
|
115
|
+
"""A thread-safe connection pool for ShotGrid API."""
|
|
116
|
+
|
|
117
|
+
_instance: Optional["ShotGridConnectionPool"] = None
|
|
118
|
+
_lock: threading.Lock = threading.Lock()
|
|
119
|
+
_pool_size: int = 10
|
|
120
|
+
_connection_queue: queue.Queue[Shotgun]
|
|
121
|
+
_initialized: bool = False
|
|
122
|
+
_factory: ShotgunClientFactory
|
|
123
|
+
|
|
124
|
+
def __new__(
|
|
125
|
+
cls: Type["ShotGridConnectionPool"], factory: Optional[ShotgunClientFactory] = None
|
|
126
|
+
) -> "ShotGridConnectionPool":
|
|
127
|
+
"""Create a singleton instance of the connection pool.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
factory: Factory for creating ShotGrid clients
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
ShotGridConnectionPool: The singleton instance.
|
|
134
|
+
"""
|
|
135
|
+
with cls._lock:
|
|
136
|
+
if cls._instance is None:
|
|
137
|
+
cls._instance = super().__new__(cls)
|
|
138
|
+
cls._instance._pool_size = 10
|
|
139
|
+
cls._instance._connection_queue = queue.Queue(maxsize=10)
|
|
140
|
+
cls._instance._initialized = False
|
|
141
|
+
cls._instance._factory = factory or cls._create_default_factory()
|
|
142
|
+
logger.debug("Created new connection pool instance")
|
|
143
|
+
return cls._instance
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _create_default_factory() -> ShotgunClientFactory:
|
|
147
|
+
"""Create the default ShotGrid client factory.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
ShotgunClientFactory: The default factory instance.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
ValueError: If required environment variables are missing.
|
|
154
|
+
"""
|
|
155
|
+
url = os.getenv("SHOTGRID_URL")
|
|
156
|
+
script_name = os.getenv("SCRIPT_NAME")
|
|
157
|
+
script_key = os.getenv("SCRIPT_KEY")
|
|
158
|
+
|
|
159
|
+
if not all([url, script_name, script_key]):
|
|
160
|
+
logger.error("Missing required environment variables for ShotGrid connection")
|
|
161
|
+
logger.debug("SHOTGRID_URL: %s", url)
|
|
162
|
+
logger.debug("SCRIPT_NAME: %s", script_name)
|
|
163
|
+
logger.debug("SCRIPT_KEY: %s", script_key)
|
|
164
|
+
raise ValueError("Missing required environment variables for ShotGrid connection")
|
|
165
|
+
|
|
166
|
+
# At this point, we know these values are not None
|
|
167
|
+
assert url is not None
|
|
168
|
+
assert script_name is not None
|
|
169
|
+
assert script_key is not None
|
|
170
|
+
|
|
171
|
+
return RealShotgunFactory(
|
|
172
|
+
url=url,
|
|
173
|
+
script_name=script_name,
|
|
174
|
+
script_key=script_key,
|
|
175
|
+
http_proxy=os.getenv("SHOTGUN_HTTP_PROXY"),
|
|
176
|
+
ca_certs=os.getenv("SHOTGUN_API_CACERTS"),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def __init__(self, factory: Optional[ShotgunClientFactory] = None) -> None:
|
|
180
|
+
"""Initialize the connection pool.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
factory: Factory for creating ShotGrid clients
|
|
184
|
+
"""
|
|
185
|
+
if factory:
|
|
186
|
+
self._factory = factory
|
|
187
|
+
if not self._initialized:
|
|
188
|
+
self._init_pool()
|
|
189
|
+
self._initialized = True
|
|
190
|
+
|
|
191
|
+
def _init_pool(self) -> None:
|
|
192
|
+
"""Initialize the connection pool with connections."""
|
|
193
|
+
try:
|
|
194
|
+
for i in range(self._pool_size):
|
|
195
|
+
connection = self._factory.create_client()
|
|
196
|
+
self._connection_queue.put(connection)
|
|
197
|
+
logger.debug("Added connection %d/%d to pool", i + 1, self._pool_size)
|
|
198
|
+
logger.info("Successfully initialized connection pool with %d connections", self._pool_size)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error("Failed to initialize connection pool: %s", str(e), exc_info=True)
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
def get_connection(self, timeout: Optional[float] = None) -> Shotgun:
|
|
204
|
+
"""Get a connection from the pool.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
timeout: How long to wait for a connection if none are available.
|
|
208
|
+
If None, wait indefinitely.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Shotgun: A ShotGrid connection from the pool.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
queue.Empty: If no connection is available within the timeout period.
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
connection = self._connection_queue.get(timeout=timeout)
|
|
218
|
+
logger.debug("Got connection from pool (available: %d)", self._connection_queue.qsize())
|
|
219
|
+
return connection
|
|
220
|
+
except queue.Empty:
|
|
221
|
+
logger.error("Failed to get connection from pool: timeout after %s seconds", timeout)
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
def return_connection(self, connection: Shotgun) -> None:
|
|
225
|
+
"""Return a connection to the pool.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
connection: Connection to return to the pool.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
self._connection_queue.put(connection)
|
|
232
|
+
logger.debug("Returned connection to pool (available: %d)", self._connection_queue.qsize())
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error("Failed to return connection to pool: %s", str(e), exc_info=True)
|
|
235
|
+
raise
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ShotGridConnectionContext:
|
|
239
|
+
"""Context manager for safely handling ShotGrid connections."""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
pool: Optional[ShotGridConnectionPool] = None,
|
|
244
|
+
factory: Optional[ShotgunClientFactory] = None,
|
|
245
|
+
timeout: Optional[float] = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Initialize the context manager.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
pool: The connection pool to get connections from. If None, creates a new pool.
|
|
251
|
+
factory: Factory for creating ShotGrid clients. If provided, creates a new pool with this factory.
|
|
252
|
+
timeout: How long to wait for a connection if none are available.
|
|
253
|
+
"""
|
|
254
|
+
self.pool = pool if pool is not None else ShotGridConnectionPool(factory)
|
|
255
|
+
self.timeout = timeout
|
|
256
|
+
self.connection: Optional[Shotgun] = None
|
|
257
|
+
|
|
258
|
+
def __enter__(self) -> Shotgun:
|
|
259
|
+
"""Get a connection from the pool.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Shotgun: A ShotGrid connection from the pool.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
Exception: If connection acquisition fails.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
self.connection = self.pool.get_connection(timeout=self.timeout)
|
|
269
|
+
return self.connection
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error("Failed to acquire connection: %s", str(e), exc_info=True)
|
|
272
|
+
raise
|
|
273
|
+
|
|
274
|
+
def __exit__(
|
|
275
|
+
self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[Any]
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Return the connection to the pool."""
|
|
278
|
+
if self.connection:
|
|
279
|
+
self.pool.return_connection(self.connection)
|
|
280
|
+
self.connection = None
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Constants module for ShotGrid server.
|
|
2
|
+
|
|
3
|
+
This module contains all constant values used throughout the ShotGrid server application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# HTTP Status Codes
|
|
7
|
+
HTTP_200_OK = 200
|
|
8
|
+
HTTP_201_CREATED = 201
|
|
9
|
+
HTTP_400_BAD_REQUEST = 400
|
|
10
|
+
HTTP_404_NOT_FOUND = 404
|
|
11
|
+
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
|
12
|
+
|
|
13
|
+
# Environment Variables
|
|
14
|
+
ENV_SHOTGRID_URL = "SHOTGRID_URL"
|
|
15
|
+
ENV_SCRIPT_KEY = "SCRIPT_KEY"
|
|
16
|
+
ENV_SCRIPT_NAME = "SCRIPT_NAME"
|
|
17
|
+
|
|
18
|
+
# Common entity types
|
|
19
|
+
DEFAULT_ENTITY_TYPES = [
|
|
20
|
+
"Version",
|
|
21
|
+
"Shot",
|
|
22
|
+
"Asset",
|
|
23
|
+
"Task",
|
|
24
|
+
"Sequence",
|
|
25
|
+
"Project",
|
|
26
|
+
"Scene",
|
|
27
|
+
"CustomEntity01",
|
|
28
|
+
"CustomEntity02",
|
|
29
|
+
"CustomEntity03",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Custom entity types can be added through environment variables
|
|
33
|
+
ENV_CUSTOM_ENTITY_TYPES = "SHOTGRID_CUSTOM_ENTITY_TYPES" # Comma-separated list of custom entity types
|
|
34
|
+
ENTITY_TYPES_ENV_VAR = ENV_CUSTOM_ENTITY_TYPES # Alias for backward compatibility
|
|
35
|
+
|
|
36
|
+
# Batch operation limits
|
|
37
|
+
MAX_BATCH_SIZE = 100 # Maximum number of operations per batch request
|
|
38
|
+
MAX_FUZZY_RANGE = 1000 # Maximum range for fuzzy ID searches
|
|
39
|
+
MAX_ID_RANGE = 10000 # Maximum range for ID-based searches
|
|
40
|
+
|
|
41
|
+
# ShotGrid API Credentials
|
|
42
|
+
ENV_SHOTGRID_URL = "SHOTGRID_URL"
|
|
43
|
+
ENV_SCRIPT_NAME = "SCRIPT_NAME"
|
|
44
|
+
ENV_SCRIPT_KEY = "SCRIPT_KEY"
|
|
45
|
+
|
|
46
|
+
# API Routes
|
|
47
|
+
API_PREFIX = "/api/v1"
|
|
48
|
+
HEALTH_CHECK = "/health"
|
|
49
|
+
ENTITY = "/entity"
|
|
50
|
+
ENTITIES = "/entities"
|
|
51
|
+
DOWNLOAD = "/download"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Logging configuration for the ShotGrid MCP server.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized logging configuration for the entire application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Import built-in modules
|
|
7
|
+
import logging
|
|
8
|
+
import logging.handlers
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# Import third-party modules
|
|
15
|
+
from platformdirs import PlatformDirs
|
|
16
|
+
|
|
17
|
+
# Create platform dirs instance
|
|
18
|
+
dirs = PlatformDirs("shotgrid-mcp-server")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
22
|
+
"""Get a logger instance.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name: The name of the logger. If None, returns the root logger.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
logging.Logger: A logger instance.
|
|
29
|
+
"""
|
|
30
|
+
logger = logging.getLogger(name)
|
|
31
|
+
if not logger.handlers:
|
|
32
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
33
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
34
|
+
handler.setFormatter(formatter)
|
|
35
|
+
logger.addHandler(handler)
|
|
36
|
+
logger.setLevel(logging.INFO)
|
|
37
|
+
return logger
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def setup_logging(log_dir: Optional[str] = None) -> None:
|
|
41
|
+
"""Set up logging configuration.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
log_dir: Optional directory to store log files. If not provided,
|
|
45
|
+
logs will be stored in the platform-specific user log directory.
|
|
46
|
+
"""
|
|
47
|
+
# Use platform-specific log directory if not specified
|
|
48
|
+
if log_dir is None:
|
|
49
|
+
log_dir = dirs.user_log_dir
|
|
50
|
+
|
|
51
|
+
# Create logs directory if it doesn't exist
|
|
52
|
+
log_path = Path(log_dir)
|
|
53
|
+
log_path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
# Generate log filename with timestamp
|
|
56
|
+
timestamp = datetime.now().strftime("%Y%m%d")
|
|
57
|
+
log_file = log_path / f"shotgrid_mcp_server_{timestamp}.log"
|
|
58
|
+
|
|
59
|
+
# Create formatters
|
|
60
|
+
file_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(pathname)s:%(lineno)d - %(message)s")
|
|
61
|
+
console_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
62
|
+
|
|
63
|
+
# Create file handler with rotation
|
|
64
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
65
|
+
str(log_file), # Convert Path to str for compatibility
|
|
66
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
67
|
+
backupCount=5,
|
|
68
|
+
encoding="utf-8",
|
|
69
|
+
)
|
|
70
|
+
file_handler.setFormatter(file_formatter)
|
|
71
|
+
file_handler.setLevel(logging.DEBUG)
|
|
72
|
+
|
|
73
|
+
# Create console handler
|
|
74
|
+
console_handler = logging.StreamHandler()
|
|
75
|
+
console_handler.setFormatter(console_formatter)
|
|
76
|
+
console_handler.setLevel(logging.INFO)
|
|
77
|
+
|
|
78
|
+
# Configure root logger
|
|
79
|
+
root_logger = logging.getLogger()
|
|
80
|
+
root_logger.setLevel(logging.DEBUG)
|
|
81
|
+
|
|
82
|
+
# Remove existing handlers to avoid duplicates
|
|
83
|
+
root_logger.handlers.clear()
|
|
84
|
+
|
|
85
|
+
# Add handlers
|
|
86
|
+
root_logger.addHandler(file_handler)
|
|
87
|
+
root_logger.addHandler(console_handler)
|
|
88
|
+
|
|
89
|
+
# Create logger for this application
|
|
90
|
+
logger = logging.getLogger("mcp_shotgrid_server")
|
|
91
|
+
logger.info("Logging system initialized. Log file: %s", log_file)
|