titlemcp 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.
Files changed (62) hide show
  1. title_mcp/__init__.py +7 -0
  2. title_mcp/_version.py +8 -0
  3. title_mcp/adapters/__init__.py +28 -0
  4. title_mcp/adapters/base.py +60 -0
  5. title_mcp/adapters/configurable.py +66 -0
  6. title_mcp/adapters/default.py +257 -0
  7. title_mcp/adapters/registry.py +76 -0
  8. title_mcp/capabilities/__init__.py +12 -0
  9. title_mcp/capabilities/base.py +52 -0
  10. title_mcp/capabilities/registry.py +50 -0
  11. title_mcp/client/__init__.py +3 -0
  12. title_mcp/client/ollama_bridge.py +103 -0
  13. title_mcp/domain/__init__.py +161 -0
  14. title_mcp/domain/auditor.py +205 -0
  15. title_mcp/domain/commitment.py +237 -0
  16. title_mcp/domain/exam.py +250 -0
  17. title_mcp/domain/models.py +249 -0
  18. title_mcp/domain/parcel.py +160 -0
  19. title_mcp/domain/responses.py +29 -0
  20. title_mcp/domain/title.py +148 -0
  21. title_mcp/mcp/__init__.py +25 -0
  22. title_mcp/mcp/server.py +557 -0
  23. title_mcp/mcp/tool_catalog.py +688 -0
  24. title_mcp/mcp/toolsets.py +31 -0
  25. title_mcp/observability/__init__.py +4 -0
  26. title_mcp/observability/logging.py +70 -0
  27. title_mcp/observability/tracing.py +36 -0
  28. title_mcp/platform.py +114 -0
  29. title_mcp/plugins/__init__.py +4 -0
  30. title_mcp/plugins/base.py +26 -0
  31. title_mcp/plugins/loader.py +14 -0
  32. title_mcp/py.typed +1 -0
  33. title_mcp/services/__init__.py +43 -0
  34. title_mcp/services/clause_sets.py +251 -0
  35. title_mcp/services/commitment.py +643 -0
  36. title_mcp/services/document_analysis.py +42 -0
  37. title_mcp/services/exam.py +610 -0
  38. title_mcp/services/exam_extraction.py +1284 -0
  39. title_mcp/services/form_fingerprint.py +169 -0
  40. title_mcp/services/workflows.py +124 -0
  41. title_mcp/settings.py +94 -0
  42. title_mcp/sources/__init__.py +48 -0
  43. title_mcp/sources/base.py +89 -0
  44. title_mcp/sources/hoa_serpapi.py +1175 -0
  45. title_mcp/sources/pacer.py +599 -0
  46. title_mcp/sources/registry.py +54 -0
  47. title_mcp/sources/regrid.py +1112 -0
  48. title_mcp/state/__init__.py +11 -0
  49. title_mcp/state/base.py +26 -0
  50. title_mcp/state/factory.py +16 -0
  51. title_mcp/state/memory.py +49 -0
  52. title_mcp/state/postgres.py +136 -0
  53. title_mcp/vendors/__init__.py +20 -0
  54. title_mcp/vendors/base.py +82 -0
  55. title_mcp/vendors/registry.py +57 -0
  56. title_mcp/workflows/__init__.py +3 -0
  57. title_mcp/workflows/engine.py +230 -0
  58. titlemcp-0.1.0.dist-info/METADATA +398 -0
  59. titlemcp-0.1.0.dist-info/RECORD +62 -0
  60. titlemcp-0.1.0.dist-info/WHEEL +4 -0
  61. titlemcp-0.1.0.dist-info/entry_points.txt +3 -0
  62. titlemcp-0.1.0.dist-info/licenses/LICENSE +7 -0
title_mcp/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Title operations MCP platform."""
2
+
3
+ from title_mcp._version import __version__
4
+ from title_mcp.platform import TitleMCPPlatform
5
+ from title_mcp.settings import TitleMCPSettings
6
+
7
+ __all__ = ["TitleMCPPlatform", "TitleMCPSettings", "__version__"]
title_mcp/_version.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("titlemcp")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.1.0"
@@ -0,0 +1,28 @@
1
+ from title_mcp.adapters.base import AdapterPlan, JurisdictionAdapter, JurisdictionScope
2
+ from title_mcp.adapters.configurable import (
3
+ ConfigurableJurisdictionAdapter,
4
+ ConfiguredJurisdictionAdapterSpec,
5
+ load_configured_adapters,
6
+ )
7
+ from title_mcp.adapters.default import (
8
+ BaltimoreCityPublicRecordsAdapter,
9
+ FloridaTitleAdapter,
10
+ GenericTitleAdapter,
11
+ MiamiDadePublicRecordsAdapter,
12
+ )
13
+ from title_mcp.adapters.registry import AdapterRegistry, create_default_adapter_registry
14
+
15
+ __all__ = [
16
+ "AdapterPlan",
17
+ "AdapterRegistry",
18
+ "BaltimoreCityPublicRecordsAdapter",
19
+ "ConfigurableJurisdictionAdapter",
20
+ "ConfiguredJurisdictionAdapterSpec",
21
+ "FloridaTitleAdapter",
22
+ "GenericTitleAdapter",
23
+ "JurisdictionAdapter",
24
+ "JurisdictionScope",
25
+ "MiamiDadePublicRecordsAdapter",
26
+ "create_default_adapter_registry",
27
+ "load_configured_adapters",
28
+ ]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from title_mcp.domain.models import (
8
+ Jurisdiction,
9
+ RiskLevel,
10
+ WorkflowAction,
11
+ WorkflowKind,
12
+ WorkflowRequest,
13
+ )
14
+
15
+
16
+ class JurisdictionScope(BaseModel):
17
+ country: str | None = None
18
+ state: str | None = None
19
+ county: str | None = None
20
+ municipality: str | None = None
21
+
22
+ def matches(self, jurisdiction: Jurisdiction) -> bool:
23
+ return jurisdiction.matches(
24
+ country=self.country,
25
+ state=self.state,
26
+ county=self.county,
27
+ municipality=self.municipality,
28
+ )
29
+
30
+ @property
31
+ def specificity(self) -> int:
32
+ return sum(
33
+ 1
34
+ for value in [self.country, self.state, self.county, self.municipality]
35
+ if value
36
+ )
37
+
38
+
39
+ class AdapterPlan(BaseModel):
40
+ adapter_id: str
41
+ jurisdiction: Jurisdiction
42
+ kind: WorkflowKind
43
+ required_review: bool = True
44
+ risk_level: RiskLevel = RiskLevel.MEDIUM
45
+ steps: list[WorkflowAction] = Field(default_factory=list)
46
+ external_requirements: list[str] = Field(default_factory=list)
47
+ warnings: list[str] = Field(default_factory=list)
48
+ routing_hints: dict[str, str] = Field(default_factory=dict)
49
+
50
+
51
+ class JurisdictionAdapter(Protocol):
52
+ adapter_id: str
53
+ priority: int
54
+ workflow_kinds: frozenset[WorkflowKind] | None
55
+
56
+ def supports(self, jurisdiction: Jurisdiction) -> bool:
57
+ """Return true when this adapter handles the jurisdiction."""
58
+
59
+ async def plan(self, request: WorkflowRequest) -> AdapterPlan:
60
+ """Create jurisdiction-aware workflow steps for a request."""
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from title_mcp.adapters.base import AdapterPlan, JurisdictionScope
9
+ from title_mcp.domain.models import (
10
+ Jurisdiction,
11
+ RiskLevel,
12
+ WorkflowAction,
13
+ WorkflowKind,
14
+ WorkflowRequest,
15
+ )
16
+
17
+
18
+ class ConfiguredJurisdictionAdapterSpec(BaseModel):
19
+ adapter_id: str
20
+ scope: JurisdictionScope
21
+ workflow_kinds: frozenset[WorkflowKind] | None = None
22
+ priority: int = 100
23
+ required_review: bool = True
24
+ risk_level: RiskLevel = RiskLevel.MEDIUM
25
+ steps: list[WorkflowAction] = Field(default_factory=list)
26
+ external_requirements: list[str] = Field(default_factory=list)
27
+ warnings: list[str] = Field(default_factory=list)
28
+ routing_hints: dict[str, str] = Field(default_factory=dict)
29
+
30
+
31
+ class ConfiguredAdaptersFile(BaseModel):
32
+ adapters: list[ConfiguredJurisdictionAdapterSpec] = Field(default_factory=list)
33
+
34
+
35
+ class ConfigurableJurisdictionAdapter:
36
+ def __init__(self, spec: ConfiguredJurisdictionAdapterSpec) -> None:
37
+ self._spec = spec
38
+ self.adapter_id = spec.adapter_id
39
+ self.priority = spec.priority
40
+ self.workflow_kinds = spec.workflow_kinds
41
+
42
+ def supports(self, jurisdiction: Jurisdiction) -> bool:
43
+ return self._spec.scope.matches(jurisdiction)
44
+
45
+ async def plan(self, request: WorkflowRequest) -> AdapterPlan:
46
+ return AdapterPlan(
47
+ adapter_id=self.adapter_id,
48
+ jurisdiction=request.jurisdiction,
49
+ kind=request.kind,
50
+ required_review=request.require_human_review and self._spec.required_review,
51
+ risk_level=self._spec.risk_level,
52
+ steps=[step.model_copy(deep=True) for step in self._spec.steps],
53
+ external_requirements=list(self._spec.external_requirements),
54
+ warnings=list(self._spec.warnings),
55
+ routing_hints={
56
+ "country": request.jurisdiction.country,
57
+ **self._spec.routing_hints,
58
+ },
59
+ )
60
+
61
+
62
+ def load_configured_adapters(path: str | Path) -> list[ConfigurableJurisdictionAdapter]:
63
+ with Path(path).open("r", encoding="utf-8") as config_file:
64
+ payload = json.load(config_file)
65
+ config = ConfiguredAdaptersFile.model_validate(payload)
66
+ return [ConfigurableJurisdictionAdapter(spec) for spec in config.adapters]
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ from title_mcp.adapters.base import AdapterPlan, JurisdictionScope
4
+ from title_mcp.domain.models import (
5
+ Jurisdiction,
6
+ RiskLevel,
7
+ WorkflowAction,
8
+ WorkflowKind,
9
+ WorkflowRequest,
10
+ )
11
+
12
+ _GENERIC_STEPS: dict[WorkflowKind, list[tuple[str, str]]] = {
13
+ WorkflowKind.DOCUMENT_ANALYSIS: [
14
+ ("Capture document source", "Record source URI and expected document type."),
15
+ ("Run extraction provider", "Use configured OCR/extraction provider when available."),
16
+ (
17
+ "Review extracted facts",
18
+ "Human reviewer confirms material fields before downstream use.",
19
+ ),
20
+ ],
21
+ WorkflowKind.PUBLIC_RECORDS_SEARCH: [
22
+ ("Resolve recording authority", "Confirm county, city, and public-record source."),
23
+ ("Prepare search criteria", "Collect party names, parcel, address, and date range."),
24
+ ("Run configured search", "Use the jurisdiction-specific public-record source."),
25
+ ("Review hits", "Human reviewer confirms matches before downstream use."),
26
+ ],
27
+ WorkflowKind.HOA_ESTOPPEL: [
28
+ ("Identify association", "Confirm HOA or management company from order documents."),
29
+ ("Prepare request packet", "Gather property, owner, closing, and authorization details."),
30
+ ("Track external response", "Monitor received estoppel, fees, expiration, and exceptions."),
31
+ ],
32
+ WorkflowKind.MUNICIPAL_LIEN_SEARCH: [
33
+ ("Resolve municipality", "Confirm municipality and utility providers for the property."),
34
+ ("Submit search request", "Route request through configured municipal channel."),
35
+ (
36
+ "Review search result",
37
+ "Reviewer validates open permits, code liens, and utility balances.",
38
+ ),
39
+ ],
40
+ WorkflowKind.TAX_CERTIFICATE: [
41
+ ("Identify tax authority", "Resolve tax collector or county source for the parcel."),
42
+ ("Request certificate", "Submit request using configured authority-specific channel."),
43
+ (
44
+ "Validate taxes due",
45
+ "Reviewer confirms taxes, discounts, delinquencies, and exemptions.",
46
+ ),
47
+ ],
48
+ WorkflowKind.RELEASE_TRACKING: [
49
+ ("Capture paid lien", "Record lender, recording data, payoff, and expected release date."),
50
+ ("Schedule follow-up", "Create follow-up milestones until release is recorded."),
51
+ ("Verify recording", "Reviewer confirms release document matches the satisfied lien."),
52
+ ],
53
+ WorkflowKind.PAYOFF_PARSING: [
54
+ (
55
+ "Extract payoff fields",
56
+ "Parse lender, good-through date, per diem, and wire instructions.",
57
+ ),
58
+ (
59
+ "Flag risk fields",
60
+ "Highlight conflicting dates, wiring changes, and missing authorizations.",
61
+ ),
62
+ ("Reviewer approval", "Human reviewer confirms extracted payoff terms before use."),
63
+ ],
64
+ WorkflowKind.TITLE_CURATIVE: [
65
+ ("Summarize defect", "Capture curative issue, affected parties, and source document."),
66
+ ("Suggest evidence requests", "Draft reviewer-facing evidence and document requests."),
67
+ ("Escalate for decision", "Route legal or underwriting decisions to authorized personnel."),
68
+ ],
69
+ WorkflowKind.DOCUMENT_CLASSIFICATION: [
70
+ (
71
+ "Classify document",
72
+ "Classify uploaded document against configured title operation taxonomy.",
73
+ ),
74
+ ("Extract candidate fields", "Extract fields relevant to the classification."),
75
+ ("Review classification", "Reviewer approves classification before workflow routing."),
76
+ ],
77
+ WorkflowKind.CHECKLIST_PACKET: [
78
+ ("Assemble checklist", "Generate jurisdiction and transaction specific checklist items."),
79
+ ("Collect packet inputs", "Identify required documents and unresolved dependencies."),
80
+ ("Reviewer signoff", "Human reviewer confirms packet completeness."),
81
+ ],
82
+ WorkflowKind.STATUS_TRACKING: [
83
+ ("Normalize status", "Convert vendor or system status into internal status vocabulary."),
84
+ ("Identify blockers", "Flag missing documents, external waits, or review requirements."),
85
+ ("Publish update", "Prepare status update for downstream systems or staff."),
86
+ ],
87
+ }
88
+
89
+
90
+ class GenericTitleAdapter:
91
+ adapter_id = "generic"
92
+ priority = 0
93
+ workflow_kinds: frozenset[WorkflowKind] | None = None
94
+
95
+ def supports(self, jurisdiction: Jurisdiction) -> bool:
96
+ return True
97
+
98
+ async def plan(self, request: WorkflowRequest) -> AdapterPlan:
99
+ steps = [
100
+ WorkflowAction(label=label, description=description)
101
+ for label, description in _GENERIC_STEPS[request.kind]
102
+ ]
103
+ return AdapterPlan(
104
+ adapter_id=self.adapter_id,
105
+ jurisdiction=request.jurisdiction,
106
+ kind=request.kind,
107
+ required_review=request.require_human_review,
108
+ risk_level=RiskLevel.MEDIUM,
109
+ steps=steps,
110
+ warnings=[
111
+ "Generic adapter selected; configure a jurisdiction adapter for production routing."
112
+ ],
113
+ )
114
+
115
+
116
+ class FloridaTitleAdapter(GenericTitleAdapter):
117
+ adapter_id = "florida"
118
+ priority = 50
119
+ scope = JurisdictionScope(country="US", state="FL")
120
+
121
+ def supports(self, jurisdiction: Jurisdiction) -> bool:
122
+ return self.scope.matches(jurisdiction)
123
+
124
+ async def plan(self, request: WorkflowRequest) -> AdapterPlan:
125
+ plan = await super().plan(request)
126
+ plan.adapter_id = self.adapter_id
127
+ plan.warnings = []
128
+
129
+ if request.kind == WorkflowKind.MUNICIPAL_LIEN_SEARCH:
130
+ plan.external_requirements.extend(
131
+ [
132
+ "Confirm city, unincorporated county, and special district boundaries.",
133
+ "Check open permit and code enforcement sources separately where required.",
134
+ ]
135
+ )
136
+ elif request.kind == WorkflowKind.TAX_CERTIFICATE:
137
+ plan.external_requirements.append(
138
+ "Confirm county tax collector source and parcel or alternate key before request."
139
+ )
140
+ elif request.kind == WorkflowKind.HOA_ESTOPPEL:
141
+ plan.external_requirements.append(
142
+ "Confirm statutory estoppel fee, rush fee, and association contact before "
143
+ "submission."
144
+ )
145
+ elif request.kind == WorkflowKind.RELEASE_TRACKING:
146
+ plan.external_requirements.append(
147
+ "Track mortgage satisfaction recording against county official records."
148
+ )
149
+
150
+ plan.routing_hints["state"] = "FL"
151
+ if request.jurisdiction.county:
152
+ plan.routing_hints["county"] = request.jurisdiction.county
153
+ return plan
154
+
155
+
156
+ class BaltimoreCityPublicRecordsAdapter(GenericTitleAdapter):
157
+ adapter_id = "us-md-baltimore-city-public-records"
158
+ priority = 140
159
+ workflow_kinds = frozenset({WorkflowKind.PUBLIC_RECORDS_SEARCH})
160
+ scope = JurisdictionScope(
161
+ country="US",
162
+ state="MD",
163
+ county="Baltimore",
164
+ municipality="Baltimore City",
165
+ )
166
+
167
+ def supports(self, jurisdiction: Jurisdiction) -> bool:
168
+ return self.scope.matches(jurisdiction)
169
+
170
+ async def plan(self, request: WorkflowRequest) -> AdapterPlan:
171
+ return AdapterPlan(
172
+ adapter_id=self.adapter_id,
173
+ jurisdiction=request.jurisdiction,
174
+ kind=request.kind,
175
+ required_review=request.require_human_review,
176
+ risk_level=RiskLevel.MEDIUM,
177
+ steps=[
178
+ WorkflowAction(
179
+ label="Search Baltimore City land records",
180
+ description=(
181
+ "Use the Maryland land records path appropriate for Baltimore City."
182
+ ),
183
+ metadata={"source": "maryland_land_records", "jurisdiction": "Baltimore City"},
184
+ ),
185
+ WorkflowAction(
186
+ label="Search Baltimore City court and lien indexes",
187
+ description=(
188
+ "Check city-specific judgment, tax sale, lien, and court-related sources."
189
+ ),
190
+ metadata={"source": "baltimore_city_public_records"},
191
+ ),
192
+ WorkflowAction(
193
+ label="Review public-record matches",
194
+ description="Reviewer confirms party, property, and recording-data matches.",
195
+ ),
196
+ ],
197
+ external_requirements=[
198
+ "Confirm whether the property is Baltimore City rather than Baltimore County.",
199
+ "Capture book/page, instrument number, party name, and parcel references.",
200
+ ],
201
+ routing_hints={
202
+ "country": "US",
203
+ "state": "MD",
204
+ "county": "Baltimore",
205
+ "municipality": "Baltimore City",
206
+ },
207
+ )
208
+
209
+
210
+ class MiamiDadePublicRecordsAdapter(GenericTitleAdapter):
211
+ adapter_id = "us-fl-miami-dade-public-records"
212
+ priority = 150
213
+ workflow_kinds = frozenset({WorkflowKind.PUBLIC_RECORDS_SEARCH})
214
+
215
+ def supports(self, jurisdiction: Jurisdiction) -> bool:
216
+ return jurisdiction.matches(country="US", state="FL") and (
217
+ jurisdiction.matches(county="Miami-Dade")
218
+ or jurisdiction.matches(county="Dade")
219
+ )
220
+
221
+ async def plan(self, request: WorkflowRequest) -> AdapterPlan:
222
+ return AdapterPlan(
223
+ adapter_id=self.adapter_id,
224
+ jurisdiction=request.jurisdiction,
225
+ kind=request.kind,
226
+ required_review=request.require_human_review,
227
+ risk_level=RiskLevel.MEDIUM,
228
+ steps=[
229
+ WorkflowAction(
230
+ label="Search Miami-Dade official records",
231
+ description=(
232
+ "Use Miami-Dade specific official records search parameters and indexes."
233
+ ),
234
+ metadata={"source": "miami_dade_official_records"},
235
+ ),
236
+ WorkflowAction(
237
+ label="Check county civil and municipal sources",
238
+ description=(
239
+ "Review Miami-Dade county-specific civil, code, permit, and lien sources."
240
+ ),
241
+ metadata={"source": "miami_dade_county_sources"},
242
+ ),
243
+ WorkflowAction(
244
+ label="Review public-record matches",
245
+ description="Reviewer confirms party, property, and recording-data matches.",
246
+ ),
247
+ ],
248
+ external_requirements=[
249
+ "Normalize Dade County aliases to Miami-Dade for modern source routing.",
250
+ "Confirm municipality because municipal sources vary inside Miami-Dade County.",
251
+ ],
252
+ routing_hints={
253
+ "country": "US",
254
+ "state": "FL",
255
+ "county": "Miami-Dade",
256
+ },
257
+ )
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import entry_points
4
+ from pathlib import Path
5
+
6
+ from title_mcp.adapters.base import JurisdictionAdapter
7
+ from title_mcp.adapters.configurable import load_configured_adapters
8
+ from title_mcp.adapters.default import (
9
+ BaltimoreCityPublicRecordsAdapter,
10
+ FloridaTitleAdapter,
11
+ GenericTitleAdapter,
12
+ MiamiDadePublicRecordsAdapter,
13
+ )
14
+ from title_mcp.domain.models import Jurisdiction, WorkflowKind
15
+
16
+
17
+ class AdapterRegistry:
18
+ def __init__(self) -> None:
19
+ self._adapters: list[JurisdictionAdapter] = []
20
+
21
+ def register(self, adapter: JurisdictionAdapter) -> None:
22
+ adapter_id = getattr(adapter, "adapter_id", adapter.__class__.__qualname__)
23
+ self._adapters = [
24
+ existing
25
+ for existing in self._adapters
26
+ if getattr(existing, "adapter_id", existing.__class__.__qualname__) != adapter_id
27
+ ]
28
+ self._adapters.append(adapter)
29
+ self._adapters.sort(key=lambda item: item.priority, reverse=True)
30
+
31
+ def resolve(
32
+ self,
33
+ jurisdiction: Jurisdiction,
34
+ kind: WorkflowKind | None = None,
35
+ ) -> JurisdictionAdapter:
36
+ for adapter in self._adapters:
37
+ if _supports_kind(adapter, kind) and adapter.supports(jurisdiction):
38
+ return adapter
39
+ suffix = f" and workflow {kind.value}" if kind else ""
40
+ raise LookupError(f"No adapter registered for jurisdiction {jurisdiction.key}{suffix}")
41
+
42
+ def all(self) -> list[JurisdictionAdapter]:
43
+ return list(self._adapters)
44
+
45
+ def load_entry_points(self, group: str = "title_mcp.adapters") -> None:
46
+ for entry_point in entry_points(group=group):
47
+ adapter_cls = entry_point.load()
48
+ self.register(adapter_cls())
49
+
50
+ def load_config_file(self, path: str | Path) -> None:
51
+ for adapter in load_configured_adapters(path):
52
+ self.register(adapter)
53
+
54
+
55
+ def create_default_adapter_registry(
56
+ *,
57
+ include_entry_points: bool = False,
58
+ config_path: str | Path | None = None,
59
+ ) -> AdapterRegistry:
60
+ registry = AdapterRegistry()
61
+ registry.register(BaltimoreCityPublicRecordsAdapter())
62
+ registry.register(MiamiDadePublicRecordsAdapter())
63
+ registry.register(FloridaTitleAdapter())
64
+ registry.register(GenericTitleAdapter())
65
+ if config_path:
66
+ registry.load_config_file(config_path)
67
+ if include_entry_points:
68
+ registry.load_entry_points()
69
+ return registry
70
+
71
+
72
+ def _supports_kind(adapter: JurisdictionAdapter, kind: WorkflowKind | None) -> bool:
73
+ if kind is None:
74
+ return True
75
+ workflow_kinds = getattr(adapter, "workflow_kinds", None)
76
+ return workflow_kinds is None or kind in workflow_kinds
@@ -0,0 +1,12 @@
1
+ from title_mcp.capabilities.base import CapabilityManifest, CapabilityType
2
+ from title_mcp.capabilities.registry import (
3
+ CapabilityRegistry,
4
+ create_default_capability_registry,
5
+ )
6
+
7
+ __all__ = [
8
+ "CapabilityManifest",
9
+ "CapabilityRegistry",
10
+ "CapabilityType",
11
+ "create_default_capability_registry",
12
+ ]
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from title_mcp.adapters.base import JurisdictionScope
9
+ from title_mcp.domain.models import Jurisdiction, WorkflowKind
10
+
11
+
12
+ class CapabilityType(StrEnum):
13
+ MCP_TOOLSET = "mcp_toolset"
14
+ WORKFLOW_ADAPTER = "workflow_adapter"
15
+ GOVERNMENT_SOURCE = "government_source"
16
+ VENDOR_CONNECTOR = "vendor_connector"
17
+ DOCUMENT_ANALYZER = "document_analyzer"
18
+ SCHEMA_PACK = "schema_pack"
19
+ POLICY_PACK = "policy_pack"
20
+
21
+
22
+ class CapabilityManifest(BaseModel):
23
+ """Install-time description of a TitleMCP extension package."""
24
+
25
+ model_config = ConfigDict(str_strip_whitespace=True)
26
+
27
+ capability_id: str = Field(min_length=1)
28
+ name: str = Field(min_length=1)
29
+ version: str = Field(default="0.1.0", min_length=1)
30
+ description: str | None = None
31
+ package_name: str | None = None
32
+ capability_types: list[CapabilityType] = Field(default_factory=list)
33
+ jurisdiction_scopes: list[JurisdictionScope] = Field(default_factory=list)
34
+ workflow_kinds: list[WorkflowKind] = Field(default_factory=list)
35
+ entry_points: dict[str, str] = Field(default_factory=dict)
36
+ config_schema: dict[str, Any] = Field(default_factory=dict)
37
+ review_required: bool = True
38
+ metadata: dict[str, Any] = Field(default_factory=dict)
39
+
40
+ def supports(
41
+ self,
42
+ jurisdiction: Jurisdiction,
43
+ kind: WorkflowKind | None = None,
44
+ capability_type: CapabilityType | None = None,
45
+ ) -> bool:
46
+ if capability_type and capability_type not in self.capability_types:
47
+ return False
48
+ if kind and self.workflow_kinds and kind not in self.workflow_kinds:
49
+ return False
50
+ return not self.jurisdiction_scopes or any(
51
+ scope.matches(jurisdiction) for scope in self.jurisdiction_scopes
52
+ )
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import entry_points
4
+ from typing import Any
5
+
6
+ from title_mcp.capabilities.base import CapabilityManifest, CapabilityType
7
+ from title_mcp.domain.models import Jurisdiction, WorkflowKind
8
+
9
+
10
+ class CapabilityRegistry:
11
+ def __init__(self) -> None:
12
+ self._capabilities: dict[str, CapabilityManifest] = {}
13
+
14
+ def register(self, manifest: CapabilityManifest | dict[str, Any]) -> None:
15
+ parsed = CapabilityManifest.model_validate(manifest)
16
+ self._capabilities[parsed.capability_id] = parsed
17
+
18
+ def get(self, capability_id: str) -> CapabilityManifest | None:
19
+ return self._capabilities.get(capability_id)
20
+
21
+ def all(self) -> list[CapabilityManifest]:
22
+ return sorted(self._capabilities.values(), key=lambda manifest: manifest.capability_id)
23
+
24
+ def resolve(
25
+ self,
26
+ jurisdiction: Jurisdiction,
27
+ *,
28
+ kind: WorkflowKind | None = None,
29
+ capability_type: CapabilityType | None = None,
30
+ ) -> list[CapabilityManifest]:
31
+ return [
32
+ manifest
33
+ for manifest in self.all()
34
+ if manifest.supports(jurisdiction, kind, capability_type)
35
+ ]
36
+
37
+ def load_entry_points(self, group: str = "title_mcp.capabilities") -> None:
38
+ for entry_point in entry_points(group=group):
39
+ loaded = entry_point.load()
40
+ manifest = loaded() if callable(loaded) else loaded
41
+ self.register(manifest)
42
+
43
+
44
+ def create_default_capability_registry(
45
+ *, include_entry_points: bool = False
46
+ ) -> CapabilityRegistry:
47
+ registry = CapabilityRegistry()
48
+ if include_entry_points:
49
+ registry.load_entry_points()
50
+ return registry
@@ -0,0 +1,3 @@
1
+ from title_mcp.client.ollama_bridge import run_ollama_prompt
2
+
3
+ __all__ = ["run_ollama_prompt"]