cfgit 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.
- cfg/__init__.py +13 -0
- cfg/adapters/__init__.py +25 -0
- cfg/adapters/base.py +127 -0
- cfg/adapters/mongo.py +570 -0
- cfg/adapters/postgres.py +756 -0
- cfg/approval/__init__.py +5 -0
- cfg/approval/base.py +29 -0
- cfg/cli/__init__.py +2 -0
- cfg/cli/main.py +665 -0
- cfg/core/__init__.py +13 -0
- cfg/core/authz.py +58 -0
- cfg/core/config.py +324 -0
- cfg/core/diff.py +43 -0
- cfg/core/engine.py +1388 -0
- cfg/core/hashing.py +102 -0
- cfg/core/identity.py +213 -0
- cfg/interfaces/__init__.py +2 -0
- cfg/interfaces/actions.py +598 -0
- cfg/mcp/__init__.py +10 -0
- cfg/mcp/server.py +452 -0
- cfg/ui/__init__.py +2 -0
- cfg/ui/server.py +1066 -0
- cfgit-0.1.0.dist-info/METADATA +744 -0
- cfgit-0.1.0.dist-info/RECORD +28 -0
- cfgit-0.1.0.dist-info/WHEEL +4 -0
- cfgit-0.1.0.dist-info/entry_points.txt +3 -0
- cfgit-0.1.0.dist-info/licenses/LICENSE +201 -0
- cfgit-0.1.0.dist-info/licenses/NOTICE +10 -0
cfg/mcp/server.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# Copyright 2026 Mohammad Ausaf. Licensed under the Apache License, Version 2.0.
|
|
2
|
+
"""MCP server for cfgit."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from cfg.core.identity import MIN_TOKEN_LENGTH, hash_token
|
|
8
|
+
from cfg.interfaces import actions
|
|
9
|
+
from cfg.interfaces.actions import ActionContext
|
|
10
|
+
|
|
11
|
+
try: # pragma: no cover - exercised when cfgit[mcp] is installed
|
|
12
|
+
from mcp.server.fastmcp import FastMCP
|
|
13
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
14
|
+
FastMCP = None # type: ignore[assignment]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _mcp() -> Any:
|
|
18
|
+
if FastMCP is None:
|
|
19
|
+
raise ModuleNotFoundError("install cfgit[mcp] to run the cfgit MCP server")
|
|
20
|
+
return FastMCP("cfgit")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
mcp = _mcp()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
def cfg_whoami(config_file: str | None = None, env: str = "dev", author: str | None = None) -> dict[str, Any]:
|
|
28
|
+
return _call("whoami", {}, config_file=config_file, env=env, author=author)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@mcp.tool()
|
|
32
|
+
def cfg_init(config_file: str | None = None, env: str = "dev", author: str | None = None) -> dict[str, Any]:
|
|
33
|
+
return _call("init", {}, config_file=config_file, env=env, author=author)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
def cfg_status(
|
|
38
|
+
record: str | None = None,
|
|
39
|
+
config_file: str | None = None,
|
|
40
|
+
env: str = "dev",
|
|
41
|
+
author: str | None = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
return _call("status", {"record": record}, config_file=config_file, env=env, author=author)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool()
|
|
47
|
+
def cfg_doctor(
|
|
48
|
+
record: str | None = None,
|
|
49
|
+
large_field_bytes: int = 20000,
|
|
50
|
+
config_file: str | None = None,
|
|
51
|
+
env: str = "dev",
|
|
52
|
+
author: str | None = None,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""Read-only preflight before import/commit: reports secret-deny matches,
|
|
55
|
+
oversized fields, and key issues per collection, with paste-ready secret_fields
|
|
56
|
+
/ ignore_fields snippets. Writes nothing."""
|
|
57
|
+
return _call(
|
|
58
|
+
"doctor",
|
|
59
|
+
{"record": record, "large_field_bytes": large_field_bytes},
|
|
60
|
+
config_file=config_file,
|
|
61
|
+
env=env,
|
|
62
|
+
author=author,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
def cfg_import(
|
|
68
|
+
record: str | None = None,
|
|
69
|
+
all_records: bool = False,
|
|
70
|
+
message: str = "initial import",
|
|
71
|
+
allow_secret: bool = False,
|
|
72
|
+
config_file: str | None = None,
|
|
73
|
+
env: str = "dev",
|
|
74
|
+
author: str | None = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
return _call(
|
|
77
|
+
"import",
|
|
78
|
+
{
|
|
79
|
+
"record": record,
|
|
80
|
+
"all_records": all_records,
|
|
81
|
+
"message": message,
|
|
82
|
+
"allow_secret": allow_secret,
|
|
83
|
+
},
|
|
84
|
+
config_file=config_file,
|
|
85
|
+
env=env,
|
|
86
|
+
author=author,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@mcp.tool()
|
|
91
|
+
def cfg_diff(
|
|
92
|
+
record: str,
|
|
93
|
+
a: str = "=HEAD",
|
|
94
|
+
b: str = "=live",
|
|
95
|
+
config_file: str | None = None,
|
|
96
|
+
env: str = "dev",
|
|
97
|
+
author: str | None = None,
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
return _call(
|
|
100
|
+
"diff",
|
|
101
|
+
{"record": record, "a": a, "b": b},
|
|
102
|
+
config_file=config_file,
|
|
103
|
+
env=env,
|
|
104
|
+
author=author,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mcp.tool()
|
|
109
|
+
def cfg_impact(
|
|
110
|
+
record: str,
|
|
111
|
+
a: str = "=HEAD",
|
|
112
|
+
b: str = "=live",
|
|
113
|
+
use_llm: bool = False,
|
|
114
|
+
provider: str | None = None,
|
|
115
|
+
model: str | None = None,
|
|
116
|
+
against: list[str] | str | None = None,
|
|
117
|
+
config_file: str | None = None,
|
|
118
|
+
env: str = "dev",
|
|
119
|
+
author: str | None = None,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
return _call(
|
|
122
|
+
"impact",
|
|
123
|
+
{
|
|
124
|
+
"record": record,
|
|
125
|
+
"a": a,
|
|
126
|
+
"b": b,
|
|
127
|
+
"use_llm": use_llm,
|
|
128
|
+
"provider": provider,
|
|
129
|
+
"model": model,
|
|
130
|
+
"against": against,
|
|
131
|
+
},
|
|
132
|
+
config_file=config_file,
|
|
133
|
+
env=env,
|
|
134
|
+
author=author,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@mcp.tool()
|
|
139
|
+
def cfg_commit(
|
|
140
|
+
record: str,
|
|
141
|
+
doc_json: str,
|
|
142
|
+
message: str,
|
|
143
|
+
allow_secret: bool = False,
|
|
144
|
+
branch: str | None = None,
|
|
145
|
+
config_file: str | None = None,
|
|
146
|
+
env: str = "dev",
|
|
147
|
+
author: str | None = None,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
return _call(
|
|
150
|
+
"commit",
|
|
151
|
+
{"record": record, "doc": doc_json, "message": message, "allow_secret": allow_secret, "branch": branch},
|
|
152
|
+
config_file=config_file,
|
|
153
|
+
env=env,
|
|
154
|
+
author=author,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@mcp.tool()
|
|
159
|
+
def cfg_bulk_commit(
|
|
160
|
+
items: list[dict[str, Any]] | dict[str, Any] | str,
|
|
161
|
+
message: str,
|
|
162
|
+
allow_secret: bool = False,
|
|
163
|
+
branch: str | None = None,
|
|
164
|
+
config_file: str | None = None,
|
|
165
|
+
env: str = "dev",
|
|
166
|
+
author: str | None = None,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Commit multiple full documents as one batch intent.
|
|
169
|
+
|
|
170
|
+
`items` may be either:
|
|
171
|
+
[{"record":"collection:id","doc":{...}}, ...]
|
|
172
|
+
{"collection:id": {...}, ...}, or a JSON string in either shape.
|
|
173
|
+
"""
|
|
174
|
+
return _call(
|
|
175
|
+
"bulk_commit",
|
|
176
|
+
{"items": items, "message": message, "allow_secret": allow_secret, "branch": branch},
|
|
177
|
+
config_file=config_file,
|
|
178
|
+
env=env,
|
|
179
|
+
author=author,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@mcp.tool()
|
|
184
|
+
def cfg_log(
|
|
185
|
+
record: str,
|
|
186
|
+
limit: int = 20,
|
|
187
|
+
config_file: str | None = None,
|
|
188
|
+
env: str = "dev",
|
|
189
|
+
author: str | None = None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
return _call(
|
|
192
|
+
"log",
|
|
193
|
+
{"record": record, "limit": limit},
|
|
194
|
+
config_file=config_file,
|
|
195
|
+
env=env,
|
|
196
|
+
author=author,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@mcp.tool()
|
|
201
|
+
def cfg_show(
|
|
202
|
+
record: str,
|
|
203
|
+
ref: str = "HEAD",
|
|
204
|
+
config_file: str | None = None,
|
|
205
|
+
env: str = "dev",
|
|
206
|
+
author: str | None = None,
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
return _call(
|
|
209
|
+
"show",
|
|
210
|
+
{"record": record, "ref": ref},
|
|
211
|
+
config_file=config_file,
|
|
212
|
+
env=env,
|
|
213
|
+
author=author,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@mcp.tool()
|
|
218
|
+
def cfg_adopt(
|
|
219
|
+
record: str | None = None,
|
|
220
|
+
all_records: bool = False,
|
|
221
|
+
message: str = "adopt drift",
|
|
222
|
+
allow_secret: bool = False,
|
|
223
|
+
config_file: str | None = None,
|
|
224
|
+
env: str = "dev",
|
|
225
|
+
author: str | None = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
return _call(
|
|
228
|
+
"adopt",
|
|
229
|
+
{
|
|
230
|
+
"record": record,
|
|
231
|
+
"all_records": all_records,
|
|
232
|
+
"message": message,
|
|
233
|
+
"allow_secret": allow_secret,
|
|
234
|
+
},
|
|
235
|
+
config_file=config_file,
|
|
236
|
+
env=env,
|
|
237
|
+
author=author,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@mcp.tool()
|
|
242
|
+
def cfg_restore(
|
|
243
|
+
record: str | None = None,
|
|
244
|
+
ref: str | None = None,
|
|
245
|
+
as_of: str | None = None,
|
|
246
|
+
tag: str | None = None,
|
|
247
|
+
dry_run: bool = False,
|
|
248
|
+
message: str = "restore",
|
|
249
|
+
config_file: str | None = None,
|
|
250
|
+
env: str = "dev",
|
|
251
|
+
author: str | None = None,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
return _call(
|
|
254
|
+
"restore",
|
|
255
|
+
{
|
|
256
|
+
"record": record,
|
|
257
|
+
"ref": ref,
|
|
258
|
+
"as_of": as_of,
|
|
259
|
+
"tag": tag,
|
|
260
|
+
"dry_run": dry_run,
|
|
261
|
+
"message": message,
|
|
262
|
+
},
|
|
263
|
+
config_file=config_file,
|
|
264
|
+
env=env,
|
|
265
|
+
author=author,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
def cfg_tag(
|
|
271
|
+
name: str,
|
|
272
|
+
config_file: str | None = None,
|
|
273
|
+
env: str = "dev",
|
|
274
|
+
author: str | None = None,
|
|
275
|
+
) -> dict[str, Any]:
|
|
276
|
+
return _call("tag", {"name": name}, config_file=config_file, env=env, author=author)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@mcp.tool()
|
|
280
|
+
def cfg_branch_list(config_file: str | None = None, env: str = "dev", author: str | None = None) -> dict[str, Any]:
|
|
281
|
+
return _call("branch_list", {}, config_file=config_file, env=env, author=author)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@mcp.tool()
|
|
285
|
+
def cfg_branch_create(
|
|
286
|
+
name: str,
|
|
287
|
+
from_branch: str = "main",
|
|
288
|
+
message: str | None = None,
|
|
289
|
+
config_file: str | None = None,
|
|
290
|
+
env: str = "dev",
|
|
291
|
+
author: str | None = None,
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
return _call(
|
|
294
|
+
"branch_create",
|
|
295
|
+
{"name": name, "from_branch": from_branch, "message": message},
|
|
296
|
+
config_file=config_file,
|
|
297
|
+
env=env,
|
|
298
|
+
author=author,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@mcp.tool()
|
|
303
|
+
def cfg_branch_delete(
|
|
304
|
+
name: str,
|
|
305
|
+
config_file: str | None = None,
|
|
306
|
+
env: str = "dev",
|
|
307
|
+
author: str | None = None,
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
return _call("branch_delete", {"name": name}, config_file=config_file, env=env, author=author)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@mcp.tool()
|
|
313
|
+
def cfg_branch_diff(
|
|
314
|
+
range: str,
|
|
315
|
+
config_file: str | None = None,
|
|
316
|
+
env: str = "dev",
|
|
317
|
+
author: str | None = None,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
return _call("branch_diff", {"range": range}, config_file=config_file, env=env, author=author)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@mcp.tool()
|
|
323
|
+
def cfg_branch_log(
|
|
324
|
+
branch: str,
|
|
325
|
+
limit: int = 20,
|
|
326
|
+
config_file: str | None = None,
|
|
327
|
+
env: str = "dev",
|
|
328
|
+
author: str | None = None,
|
|
329
|
+
) -> dict[str, Any]:
|
|
330
|
+
return _call("branch_log", {"branch": branch, "limit": limit}, config_file=config_file, env=env, author=author)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.tool()
|
|
334
|
+
def cfg_pr_create(
|
|
335
|
+
head: str,
|
|
336
|
+
message: str,
|
|
337
|
+
base: str = "main",
|
|
338
|
+
config_file: str | None = None,
|
|
339
|
+
env: str = "dev",
|
|
340
|
+
author: str | None = None,
|
|
341
|
+
) -> dict[str, Any]:
|
|
342
|
+
return _call(
|
|
343
|
+
"pr_create",
|
|
344
|
+
{"base": base, "head": head, "message": message},
|
|
345
|
+
config_file=config_file,
|
|
346
|
+
env=env,
|
|
347
|
+
author=author,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@mcp.tool()
|
|
352
|
+
def cfg_pr_list(
|
|
353
|
+
status: str | None = None,
|
|
354
|
+
config_file: str | None = None,
|
|
355
|
+
env: str = "dev",
|
|
356
|
+
author: str | None = None,
|
|
357
|
+
) -> dict[str, Any]:
|
|
358
|
+
return _call("pr_list", {"status": status}, config_file=config_file, env=env, author=author)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@mcp.tool()
|
|
362
|
+
def cfg_pr_show(
|
|
363
|
+
id: str,
|
|
364
|
+
config_file: str | None = None,
|
|
365
|
+
env: str = "dev",
|
|
366
|
+
author: str | None = None,
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
return _call("pr_show", {"id": id}, config_file=config_file, env=env, author=author)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@mcp.tool()
|
|
372
|
+
def cfg_pr_close(
|
|
373
|
+
id: str,
|
|
374
|
+
config_file: str | None = None,
|
|
375
|
+
env: str = "dev",
|
|
376
|
+
author: str | None = None,
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
return _call("pr_close", {"id": id}, config_file=config_file, env=env, author=author)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@mcp.tool()
|
|
382
|
+
def cfg_pr_merge(
|
|
383
|
+
id: str,
|
|
384
|
+
message: str | None = None,
|
|
385
|
+
config_file: str | None = None,
|
|
386
|
+
env: str = "dev",
|
|
387
|
+
author: str | None = None,
|
|
388
|
+
) -> dict[str, Any]:
|
|
389
|
+
return _call("pr_merge", {"id": id, "message": message}, config_file=config_file, env=env, author=author)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@mcp.tool()
|
|
393
|
+
def cfg_fsck(config_file: str | None = None, env: str = "dev", author: str | None = None) -> dict[str, Any]:
|
|
394
|
+
return _call("fsck", {}, config_file=config_file, env=env, author=author)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@mcp.tool()
|
|
398
|
+
def cfg_identity_hash(token: str) -> dict[str, Any]:
|
|
399
|
+
"""Hash a private identity token for .cfg.toml setup.
|
|
400
|
+
|
|
401
|
+
Prefer the local CLI for real human secrets:
|
|
402
|
+
`printf '%s' '<token>' | cfg identity-hash --stdin`.
|
|
403
|
+
"""
|
|
404
|
+
raw = token.strip()
|
|
405
|
+
if len(raw) < MIN_TOKEN_LENGTH:
|
|
406
|
+
return {
|
|
407
|
+
"status": "error",
|
|
408
|
+
"code": actions.EXIT_ARG,
|
|
409
|
+
"message": f"identity token must be at least {MIN_TOKEN_LENGTH} characters",
|
|
410
|
+
"data": None,
|
|
411
|
+
}
|
|
412
|
+
hashed = hash_token(raw)
|
|
413
|
+
return {
|
|
414
|
+
"status": "ok",
|
|
415
|
+
"code": actions.EXIT_OK,
|
|
416
|
+
"message": "",
|
|
417
|
+
"data": {
|
|
418
|
+
"sha256": hashed,
|
|
419
|
+
"fingerprint": hashed[7:12],
|
|
420
|
+
"warning": "the raw token passed to this MCP tool may be visible to the MCP client; prefer local CLI for real secrets",
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _call(
|
|
426
|
+
name: str,
|
|
427
|
+
payload: dict[str, Any],
|
|
428
|
+
*,
|
|
429
|
+
config_file: str | None,
|
|
430
|
+
env: str,
|
|
431
|
+
author: str | None,
|
|
432
|
+
) -> dict[str, Any]:
|
|
433
|
+
return actions.envelope(_call_inner, name, payload, config_file, env, author)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _call_inner(
|
|
437
|
+
name: str,
|
|
438
|
+
payload: dict[str, Any],
|
|
439
|
+
config_file: str | None,
|
|
440
|
+
env: str,
|
|
441
|
+
author: str | None,
|
|
442
|
+
) -> tuple[Any, int]:
|
|
443
|
+
engine = actions.make_engine(ActionContext(config_file=config_file, env=env, author=author))
|
|
444
|
+
return actions.run_named_action(name, engine, payload)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def main() -> None:
|
|
448
|
+
mcp.run()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
if __name__ == "__main__":
|
|
452
|
+
main()
|
cfg/ui/__init__.py
ADDED