An MCP server for Osprey
1SYNTAX_DATA_EXTRACTION = """\
2## Data Extraction
3
4### JsonData - Extract values from event JSON
5```python
6FieldName: type = JsonData(path='$.json.path')
7FieldName: Optional[type] = JsonData(path='$.path', required=False)
8```
9
10### EntityJson - Create typed entities from JSON (for IDs that can have labels)
11```python
12UserId: Entity[str] = EntityJson(type='UserId', path='$.user.id')
13PostId: Entity[str] = EntityJson(type='PostId', path='$.postId')
14```
15
16### Supported types
17- `str`, `int`, `float`, `bool`
18- `List[str]`, `List[int]`, etc.
19- `Optional[type]` for nullable fields
20- `Entity[type]` for identifiers that can have labels attached
21"""
22
23SYNTAX_RULES = """\
24## Rules
25
26### Defining a Rule
27```python
28RuleName = Rule(
29 when_all=[
30 Condition1,
31 Condition2, # ALL conditions must be True
32 ],
33 description='What this rule detects'
34)
35```
36
37### Rule naming
38- Must be PascalCase (e.g., `SpamDetectionRule`)
39- Cannot start with underscore
40- Description must be a string literal or f-string
41"""
42
43SYNTAX_EFFECTS = """\
44## Effects and WhenRules
45
46### Wiring rules to effects
47```python
48WhenRules(
49 rules_any=[Rule1, Rule2], # ANY rule triggers ALL effects
50 then=[
51 DeclareVerdict(verdict='reject'),
52 LabelAdd(entity=UserId, label='spam'),
53 ],
54)
55```
56
57### Available effects
58- `DeclareVerdict(verdict='...')` - Return verdict to caller
59- `LabelAdd(entity=E, label='L')` - Add label to entity
60- `LabelRemove(entity=E, label='L')` - Remove label from entity
61"""
62
63SYNTAX_OPERATORS = """\
64## Operators
65
66### Comparison
67- `==`, `!=`, `>`, `>=`, `<`, `<=`
68- `in`, `not in` (for list membership)
69
70### Boolean
71- `and`, `or`, `not`
72- All conditions in `when_all` are implicitly AND-ed
73- Use parentheses for OR: `(Cond1 or Cond2)`
74
75### Null handling
76- `Value != None` - check if not null
77- `Value == None` - check if null
78- Always check for null before using optional fields
79"""
80
81SYNTAX_IMPORTS = """\
82## File Organization
83
84### Import - Include models/features from other files
85```python
86Import(rules=[
87 'models/base.sml',
88 'models/record/post.sml',
89])
90```
91- Paths must be relative and sorted lexicographically
92- Imported features become available in current file
93
94### Require - Conditionally include rule files
95```python
96Require(rule='rules/spam/check.sml')
97Require(rule='rules/post/links.sml', require_if=EventType == 'post')
98```
99- Use for conditional rule execution based on event type
100- Required file outputs are NOT available in parent file
101"""
102
103PROJECT_STRUCTURE = """\
104## Project Structure
105
106Osprey rules projects follow this structure:
107
108```
109rules/
110├── main.sml # Entry point - requires index.sml
111├── models/
112│ ├── base.sml # Common features (UserId, Handle, etc.)
113│ └── record/
114│ ├── post.sml # Post-specific features
115│ ├── like.sml # Like-specific features
116│ └── profile.sml # Profile-specific features
117└── rules/
118 ├── index.sml # Routes to event-specific rules
119 └── record/
120 ├── post/
121 │ ├── index.sml # Requires post rules
122 │ ├── spam.sml # Spam detection
123 │ └── links.sml # Link abuse detection
124 └── profile/
125 ├── index.sml
126 └── impersonation.sml
127```
128
129### Key principles
1301. **models/** - Feature definitions only, no rules
1312. **rules/** - Rule logic with WhenRules effects
1323. **index.sml** - Conditional routing based on event type
1334. Each rule file imports the models it needs
134"""
135
136PATTERN_BASIC_RULE = """\
137### Basic Rule Pattern
138```python
139Import(rules=[
140 'models/base.sml',
141 'models/record/post.sml',
142])
143
144# Define the rule
145SpamLinkRule = Rule(
146 when_all=[
147 AccountAgeSecondsUnwrapped < Day,
148 PostHasExternal,
149 PostIsReply,
150 ],
151 description='New account posting external links in replies',
152)
153
154# Wire to effects
155WhenRules(
156 rules_any=[SpamLinkRule],
157 then=[
158 LabelAdd(entity=UserId, label='reply-link-spam'),
159 ],
160)
161```
162"""
163
164PATTERN_MULTIPLE_RULES = """\
165### Multiple Rules with Tiered Response
166```python
167# Low severity
168LowRiskRule = Rule(
169 when_all=[Signal1],
170 description='Single signal detected',
171)
172
173# High severity - multiple signals
174HighRiskRule = Rule(
175 when_all=[Signal1, Signal2, Signal3],
176 description='Multiple signals detected',
177)
178
179WhenRules(
180 rules_any=[LowRiskRule, HighRiskRule],
181 then=[
182 LabelAdd(entity=UserId, label='flagged'),
183 LabelAdd(entity=UserId, label='high-risk', apply_if=HighRiskRule),
184 ],
185)
186```
187"""
188
189PATTERN_COMPUTED_FEATURES = """\
190### Computed Features
191```python
192# Compute intermediate values
193FollowRatio = FollowingCount / (FollowersCount + 1) # +1 to avoid division by zero
194IsHighFollowRatio = FollowRatio > 10.0
195
196MessageLength = StringLength(s=PostText)
197IsShortMessage = MessageLength < 10
198
199HasManyMentions = ListLength(list=FacetMentionList) > 5
200
201# Use in rules
202SuspiciousActivity = Rule(
203 when_all=[
204 IsHighFollowRatio,
205 IsShortMessage,
206 HasManyMentions,
207 ],
208 description='Suspicious activity pattern',
209)
210```
211"""
212
213PATTERN_NULL_SAFETY = """\
214### Null-Safe Patterns
215```python
216# For optional fields, always check null first
217OptionalField: Optional[str] = JsonData(path='$.maybe', required=False)
218
219SafeRule = Rule(
220 when_all=[
221 OptionalField != None, # Guard clause
222 StringLength(s=OptionalField) > 10,
223 ],
224 description='Checks optional field safely',
225)
226
227# Or use ResolveOptional for defaults
228SafeValue: str = ResolveOptional(
229 optional_value=OptionalField,
230 default_value='',
231)
232```
233"""
234
235
236def get_syntax_reference() -> str:
237 return "\n\n".join(
238 [
239 "# SML Syntax Reference",
240 SYNTAX_DATA_EXTRACTION,
241 SYNTAX_RULES,
242 SYNTAX_EFFECTS,
243 SYNTAX_OPERATORS,
244 SYNTAX_IMPORTS,
245 ]
246 )
247
248
249def get_project_structure() -> str:
250 return PROJECT_STRUCTURE
251
252
253def get_patterns_reference() -> str:
254 return "\n\n".join(
255 [
256 "# Common SML Patterns",
257 PATTERN_BASIC_RULE,
258 PATTERN_MULTIPLE_RULES,
259 PATTERN_COMPUTED_FEATURES,
260 PATTERN_NULL_SAFETY,
261 ]
262 )