mssql-agent-mcp 1.0.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.
- mssql_agent_mcp/__init__.py +9 -0
- mssql_agent_mcp/__main__.py +6 -0
- mssql_agent_mcp/config.py +25 -0
- mssql_agent_mcp/server.py +64 -0
- mssql_agent_mcp/tools/__init__.py +58 -0
- mssql_agent_mcp/tools/database.py +193 -0
- mssql_agent_mcp/tools/jobs.py +670 -0
- mssql_agent_mcp/tools/procedures.py +349 -0
- mssql_agent_mcp/utils.py +127 -0
- mssql_agent_mcp-1.0.0.dist-info/METADATA +591 -0
- mssql_agent_mcp-1.0.0.dist-info/RECORD +14 -0
- mssql_agent_mcp-1.0.0.dist-info/WHEEL +4 -0
- mssql_agent_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- mssql_agent_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Configuration and environment variables for MSSQL MCP Server."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# Security configuration
|
|
6
|
+
READONLY_MODE = os.getenv("MSSQL_READONLY", "true").lower() in ("true", "1", "yes")
|
|
7
|
+
|
|
8
|
+
# Database configuration from environment variables
|
|
9
|
+
DB_CONFIG = {
|
|
10
|
+
"server": os.getenv("MSSQL_SERVER", "localhost"),
|
|
11
|
+
"database": os.getenv("MSSQL_DATABASE", "master"),
|
|
12
|
+
"user": os.getenv("MSSQL_USER", ""),
|
|
13
|
+
"password": os.getenv("MSSQL_PASSWORD", ""),
|
|
14
|
+
"port": os.getenv("MSSQL_PORT", "1433"),
|
|
15
|
+
"driver": os.getenv("MSSQL_DRIVER", "ODBC Driver 18 for SQL Server"),
|
|
16
|
+
"encrypt": os.getenv("MSSQL_ENCRYPT", "yes"),
|
|
17
|
+
"trust_server_certificate": os.getenv("MSSQL_TRUST_SERVER_CERTIFICATE", "no"),
|
|
18
|
+
"auth_mode": os.getenv("MSSQL_AUTH_MODE", "sql"), # sql, windows, azure
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Blocked SQL statement types when in readonly mode
|
|
22
|
+
BLOCKED_STATEMENT_TYPES = {
|
|
23
|
+
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
|
|
24
|
+
"TRUNCATE", "MERGE", "EXEC", "EXECUTE", "GRANT", "REVOKE", "DENY",
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""MSSQL MCP Server - Main entry point.
|
|
3
|
+
|
|
4
|
+
Provides tools for interacting with Microsoft SQL Server databases,
|
|
5
|
+
including stored procedure management and SQL Server Agent job management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from .tools import database as db_tools
|
|
11
|
+
from .tools import jobs as job_tools
|
|
12
|
+
from .tools import procedures as proc_tools
|
|
13
|
+
|
|
14
|
+
# Create the FastMCP server instance
|
|
15
|
+
mcp = FastMCP(
|
|
16
|
+
"mssql-agent-mcp",
|
|
17
|
+
instructions="""This MCP server provides tools for interacting with Microsoft SQL Server databases.
|
|
18
|
+
|
|
19
|
+
It supports:
|
|
20
|
+
- Database queries and schema inspection
|
|
21
|
+
- Stored procedure management with export/import workflows
|
|
22
|
+
- SQL Server Agent job management with jobs-as-code workflow
|
|
23
|
+
|
|
24
|
+
Use `query` for SELECT statements, `execute` for write operations.
|
|
25
|
+
Jobs and procedures can be exported to files, edited, and pushed back to the server.
|
|
26
|
+
""",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Register database tools
|
|
30
|
+
mcp.tool()(db_tools.query)
|
|
31
|
+
mcp.tool()(db_tools.execute)
|
|
32
|
+
mcp.tool()(db_tools.list_tables)
|
|
33
|
+
mcp.tool()(db_tools.describe_table)
|
|
34
|
+
mcp.tool()(db_tools.list_databases)
|
|
35
|
+
mcp.tool()(db_tools.get_table_sample)
|
|
36
|
+
mcp.tool()(db_tools.get_table_indexes)
|
|
37
|
+
mcp.tool()(db_tools.get_foreign_keys)
|
|
38
|
+
|
|
39
|
+
# Register stored procedure tools
|
|
40
|
+
mcp.tool()(proc_tools.list_procedures)
|
|
41
|
+
mcp.tool()(proc_tools.list_all_procedures)
|
|
42
|
+
mcp.tool()(proc_tools.get_procedure_details)
|
|
43
|
+
mcp.tool()(proc_tools.get_procedure_parameters)
|
|
44
|
+
mcp.tool()(proc_tools.export_procedures_to_files)
|
|
45
|
+
mcp.tool()(proc_tools.update_procedure_from_file)
|
|
46
|
+
|
|
47
|
+
# Register SQL Server Agent job tools
|
|
48
|
+
mcp.tool()(job_tools.list_agent_jobs)
|
|
49
|
+
mcp.tool()(job_tools.get_job_steps)
|
|
50
|
+
mcp.tool()(job_tools.get_job_details)
|
|
51
|
+
mcp.tool()(job_tools.get_job_schedules)
|
|
52
|
+
mcp.tool()(job_tools.get_job_history)
|
|
53
|
+
mcp.tool()(job_tools.export_enabled_jobs_to_files)
|
|
54
|
+
mcp.tool()(job_tools.update_job_step_from_file)
|
|
55
|
+
mcp.tool()(job_tools.create_job_step_from_file)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
"""Run the MCP server."""
|
|
60
|
+
mcp.run()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""MCP tools for MSSQL Server operations."""
|
|
2
|
+
|
|
3
|
+
from .database import (
|
|
4
|
+
describe_table,
|
|
5
|
+
execute,
|
|
6
|
+
get_foreign_keys,
|
|
7
|
+
get_table_indexes,
|
|
8
|
+
get_table_sample,
|
|
9
|
+
list_databases,
|
|
10
|
+
list_tables,
|
|
11
|
+
query,
|
|
12
|
+
)
|
|
13
|
+
from .jobs import (
|
|
14
|
+
create_job_step_from_file,
|
|
15
|
+
export_enabled_jobs_to_files,
|
|
16
|
+
get_job_details,
|
|
17
|
+
get_job_history,
|
|
18
|
+
get_job_schedules,
|
|
19
|
+
get_job_steps,
|
|
20
|
+
list_agent_jobs,
|
|
21
|
+
update_job_step_from_file,
|
|
22
|
+
)
|
|
23
|
+
from .procedures import (
|
|
24
|
+
export_procedures_to_files,
|
|
25
|
+
get_procedure_details,
|
|
26
|
+
get_procedure_parameters,
|
|
27
|
+
list_all_procedures,
|
|
28
|
+
list_procedures,
|
|
29
|
+
update_procedure_from_file,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Database tools
|
|
34
|
+
"query",
|
|
35
|
+
"execute",
|
|
36
|
+
"list_tables",
|
|
37
|
+
"describe_table",
|
|
38
|
+
"list_databases",
|
|
39
|
+
"get_table_sample",
|
|
40
|
+
"get_table_indexes",
|
|
41
|
+
"get_foreign_keys",
|
|
42
|
+
# Procedure tools
|
|
43
|
+
"list_procedures",
|
|
44
|
+
"list_all_procedures",
|
|
45
|
+
"get_procedure_details",
|
|
46
|
+
"get_procedure_parameters",
|
|
47
|
+
"export_procedures_to_files",
|
|
48
|
+
"update_procedure_from_file",
|
|
49
|
+
# Job tools
|
|
50
|
+
"list_agent_jobs",
|
|
51
|
+
"get_job_steps",
|
|
52
|
+
"get_job_details",
|
|
53
|
+
"get_job_schedules",
|
|
54
|
+
"get_job_history",
|
|
55
|
+
"export_enabled_jobs_to_files",
|
|
56
|
+
"update_job_step_from_file",
|
|
57
|
+
"create_job_step_from_file",
|
|
58
|
+
]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Database query and schema inspection tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from ..config import READONLY_MODE
|
|
6
|
+
from ..utils import format_result, get_connection, rows_to_dicts, validate_query
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def query(
|
|
10
|
+
sql: Annotated[str, "The SQL query to execute (SELECT statements)"]
|
|
11
|
+
) -> str:
|
|
12
|
+
"""Execute a SQL query and return results. Use for SELECT statements."""
|
|
13
|
+
is_valid, error_msg = validate_query(sql)
|
|
14
|
+
if not is_valid:
|
|
15
|
+
return format_result({"error": error_msg})
|
|
16
|
+
|
|
17
|
+
conn = get_connection()
|
|
18
|
+
try:
|
|
19
|
+
cursor = conn.cursor()
|
|
20
|
+
cursor.execute(sql)
|
|
21
|
+
if cursor.description:
|
|
22
|
+
rows = rows_to_dicts(cursor)
|
|
23
|
+
return format_result(rows) if rows else "Query executed successfully. No rows returned."
|
|
24
|
+
return "Query executed successfully. No rows returned."
|
|
25
|
+
finally:
|
|
26
|
+
conn.close()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def execute(
|
|
30
|
+
sql: Annotated[str, "The SQL statement to execute (INSERT, UPDATE, DELETE, CREATE, etc.)"]
|
|
31
|
+
) -> str:
|
|
32
|
+
"""Execute a SQL statement (INSERT, UPDATE, DELETE, CREATE, etc.) and return affected rows count."""
|
|
33
|
+
if READONLY_MODE:
|
|
34
|
+
return format_result({
|
|
35
|
+
"error": "Write operations are disabled in read-only mode. Set MSSQL_READONLY=false to enable."
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
conn = get_connection()
|
|
39
|
+
try:
|
|
40
|
+
cursor = conn.cursor()
|
|
41
|
+
cursor.execute(sql)
|
|
42
|
+
conn.commit()
|
|
43
|
+
return f"Statement executed successfully. Rows affected: {cursor.rowcount}"
|
|
44
|
+
finally:
|
|
45
|
+
conn.close()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_tables(
|
|
49
|
+
schema: Annotated[str | None, "Schema name to filter tables (optional, defaults to all schemas)"] = None
|
|
50
|
+
) -> str:
|
|
51
|
+
"""List all tables in the current database."""
|
|
52
|
+
conn = get_connection()
|
|
53
|
+
try:
|
|
54
|
+
cursor = conn.cursor()
|
|
55
|
+
if schema:
|
|
56
|
+
sql = """
|
|
57
|
+
SELECT TABLE_SCHEMA as [schema], TABLE_NAME as [table], TABLE_TYPE as [type]
|
|
58
|
+
FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?
|
|
59
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
60
|
+
"""
|
|
61
|
+
cursor.execute(sql, (schema,))
|
|
62
|
+
else:
|
|
63
|
+
sql = """
|
|
64
|
+
SELECT TABLE_SCHEMA as [schema], TABLE_NAME as [table], TABLE_TYPE as [type]
|
|
65
|
+
FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
66
|
+
"""
|
|
67
|
+
cursor.execute(sql)
|
|
68
|
+
return format_result(rows_to_dicts(cursor))
|
|
69
|
+
finally:
|
|
70
|
+
conn.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def describe_table(
|
|
74
|
+
table: Annotated[str, "The table name to describe"],
|
|
75
|
+
schema: Annotated[str, "The schema name (defaults to 'dbo')"] = "dbo"
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Get the schema/structure of a specific table including columns, data types, and constraints."""
|
|
78
|
+
sql = """
|
|
79
|
+
SELECT
|
|
80
|
+
c.COLUMN_NAME as [column], c.DATA_TYPE as [type],
|
|
81
|
+
c.CHARACTER_MAXIMUM_LENGTH as [max_length],
|
|
82
|
+
c.NUMERIC_PRECISION as [precision], c.NUMERIC_SCALE as [scale],
|
|
83
|
+
c.IS_NULLABLE as [nullable], c.COLUMN_DEFAULT as [default],
|
|
84
|
+
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'YES' ELSE 'NO' END as [primary_key]
|
|
85
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
86
|
+
LEFT JOIN (
|
|
87
|
+
SELECT ku.TABLE_SCHEMA, ku.TABLE_NAME, ku.COLUMN_NAME
|
|
88
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
89
|
+
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku ON tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
|
90
|
+
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
|
91
|
+
) pk ON c.TABLE_SCHEMA = pk.TABLE_SCHEMA AND c.TABLE_NAME = pk.TABLE_NAME AND c.COLUMN_NAME = pk.COLUMN_NAME
|
|
92
|
+
WHERE c.TABLE_NAME = ? AND c.TABLE_SCHEMA = ?
|
|
93
|
+
ORDER BY c.ORDINAL_POSITION
|
|
94
|
+
"""
|
|
95
|
+
conn = get_connection()
|
|
96
|
+
try:
|
|
97
|
+
cursor = conn.cursor()
|
|
98
|
+
cursor.execute(sql, (table, schema))
|
|
99
|
+
return format_result(rows_to_dicts(cursor))
|
|
100
|
+
finally:
|
|
101
|
+
conn.close()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def list_databases() -> str:
|
|
105
|
+
"""List all databases on the SQL Server instance."""
|
|
106
|
+
sql = """
|
|
107
|
+
SELECT name as [database], state_desc as [state],
|
|
108
|
+
recovery_model_desc as [recovery_model], create_date as [created]
|
|
109
|
+
FROM sys.databases ORDER BY name
|
|
110
|
+
"""
|
|
111
|
+
conn = get_connection()
|
|
112
|
+
try:
|
|
113
|
+
cursor = conn.cursor()
|
|
114
|
+
cursor.execute(sql)
|
|
115
|
+
return format_result(rows_to_dicts(cursor))
|
|
116
|
+
finally:
|
|
117
|
+
conn.close()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_table_sample(
|
|
121
|
+
table: Annotated[str, "The table name"],
|
|
122
|
+
schema: Annotated[str, "The schema name (defaults to 'dbo')"] = "dbo",
|
|
123
|
+
limit: Annotated[int, "Number of rows to return (defaults to 10, max 1000)"] = 10
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Get a sample of rows from a table."""
|
|
126
|
+
safe_schema = "".join(c for c in schema if c.isalnum() or c == "_")
|
|
127
|
+
safe_table = "".join(c for c in table if c.isalnum() or c == "_")
|
|
128
|
+
safe_limit = min(max(1, int(limit)), 1000)
|
|
129
|
+
|
|
130
|
+
sql = f"SELECT TOP {safe_limit} * FROM [{safe_schema}].[{safe_table}]"
|
|
131
|
+
conn = get_connection()
|
|
132
|
+
try:
|
|
133
|
+
cursor = conn.cursor()
|
|
134
|
+
cursor.execute(sql)
|
|
135
|
+
return format_result(rows_to_dicts(cursor))
|
|
136
|
+
finally:
|
|
137
|
+
conn.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_table_indexes(
|
|
141
|
+
table: Annotated[str, "The table name"],
|
|
142
|
+
schema: Annotated[str, "The schema name (defaults to 'dbo')"] = "dbo"
|
|
143
|
+
) -> str:
|
|
144
|
+
"""Get indexes defined on a table."""
|
|
145
|
+
sql = """
|
|
146
|
+
SELECT i.name as [index_name], i.type_desc as [index_type],
|
|
147
|
+
i.is_unique as [is_unique], i.is_primary_key as [is_primary_key],
|
|
148
|
+
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) as [columns]
|
|
149
|
+
FROM sys.indexes i
|
|
150
|
+
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
151
|
+
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
152
|
+
JOIN sys.tables t ON i.object_id = t.object_id
|
|
153
|
+
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
154
|
+
WHERE t.name = ? AND s.name = ?
|
|
155
|
+
GROUP BY i.name, i.type_desc, i.is_unique, i.is_primary_key
|
|
156
|
+
ORDER BY i.name
|
|
157
|
+
"""
|
|
158
|
+
conn = get_connection()
|
|
159
|
+
try:
|
|
160
|
+
cursor = conn.cursor()
|
|
161
|
+
cursor.execute(sql, (table, schema))
|
|
162
|
+
return format_result(rows_to_dicts(cursor))
|
|
163
|
+
finally:
|
|
164
|
+
conn.close()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_foreign_keys(
|
|
168
|
+
table: Annotated[str, "The table name"],
|
|
169
|
+
schema: Annotated[str, "The schema name (defaults to 'dbo')"] = "dbo"
|
|
170
|
+
) -> str:
|
|
171
|
+
"""Get foreign key relationships for a table."""
|
|
172
|
+
sql = """
|
|
173
|
+
SELECT fk.name as [constraint_name],
|
|
174
|
+
OBJECT_SCHEMA_NAME(fk.parent_object_id) as [from_schema],
|
|
175
|
+
OBJECT_NAME(fk.parent_object_id) as [from_table],
|
|
176
|
+
COL_NAME(fkc.parent_object_id, fkc.parent_column_id) as [from_column],
|
|
177
|
+
OBJECT_SCHEMA_NAME(fk.referenced_object_id) as [to_schema],
|
|
178
|
+
OBJECT_NAME(fk.referenced_object_id) as [to_table],
|
|
179
|
+
COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) as [to_column]
|
|
180
|
+
FROM sys.foreign_keys fk
|
|
181
|
+
JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
|
182
|
+
JOIN sys.tables t ON fk.parent_object_id = t.object_id
|
|
183
|
+
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
184
|
+
WHERE t.name = ? AND s.name = ?
|
|
185
|
+
ORDER BY fk.name
|
|
186
|
+
"""
|
|
187
|
+
conn = get_connection()
|
|
188
|
+
try:
|
|
189
|
+
cursor = conn.cursor()
|
|
190
|
+
cursor.execute(sql, (table, schema))
|
|
191
|
+
return format_result(rows_to_dicts(cursor))
|
|
192
|
+
finally:
|
|
193
|
+
conn.close()
|