Skip to content

Commit e533b55

Browse files
committed
Merge branch 'main' into validate-input-schema
2 parents 6582855 + 63d4627 commit e533b55

22 files changed

+1341
-95
lines changed

servlets/obsidian/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
plugin.wasm
2+
plugin/__pycache__

servlets/obsidian/plugin/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from typing import Optional, List # noqa: F401
44
from datetime import datetime # noqa: F401
5+
import json
56
import extism # pyright: ignore
67
import plugin
7-
import json
8+
89

910
from pdk_types import (
1011
BlobResourceContents,
@@ -23,6 +24,7 @@
2324

2425
# Imports
2526

27+
2628
# Exports
2729
# The implementations for these functions is in `plugin.py`
2830

@@ -31,9 +33,10 @@
3133
# If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
3234
@extism.plugin_fn
3335
def call():
34-
data = json.loads(extism.input_str())
35-
res = plugin.call(data)
36-
extism.output(res)
36+
input = extism.input_str()
37+
input = CallToolRequest.from_json(input)
38+
res = plugin.call(input)
39+
extism.output_str(res.to_json())
3740

3841

3942
# Called by mcpx to understand how and why to use this tool.
@@ -42,4 +45,4 @@ def call():
4245
@extism.plugin_fn
4346
def describe():
4447
res = plugin.describe()
45-
extism.output(res)
48+
extism.output_str(res.to_json())

servlets/obsidian/plugin/pdk_imports.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Optional, List # noqa: F401
44
from datetime import datetime # noqa: F401
55
import extism # noqa: F401 # pyright: ignore
6+
import json
67

78

89
from pdk_types import (

servlets/obsidian/plugin/pdk_types.py

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,82 @@
55
from typing import Optional, List # noqa: F401
66
from datetime import datetime # noqa: F401
77
from dataclasses import dataclass # noqa: F401
8-
9-
import extism # noqa: F401 # pyright: ignore
8+
from dataclass_wizard import JSONWizard, skip_if_field, IS # noqa: F401
9+
from dataclass_wizard.type_def import JSONObject
10+
from base64 import b64encode, b64decode
1011

1112

1213
@dataclass
13-
class BlobResourceContents(extism.Json):
14+
class BlobResourceContents(JSONWizard):
1415
# A base64-encoded string representing the binary data of the item.
1516
blob: str
1617

17-
# The MIME type of this resource, if known.
18-
mimeType: str
19-
2018
# The URI of this resource.
2119
uri: str
2220

21+
# The MIME type of this resource, if known.
22+
mimeType: Optional[str] = skip_if_field(IS(None), default=None)
2323

24-
@dataclass
25-
class CallToolRequest(extism.Json):
26-
method: str
24+
@classmethod
25+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
26+
return o
2727

28+
def _pre_dict(self):
29+
return
30+
31+
32+
@dataclass
33+
class CallToolRequest(JSONWizard):
2834
params: Params
2935

36+
method: Optional[str] = skip_if_field(IS(None), default=None)
37+
38+
@classmethod
39+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
40+
return o
41+
42+
def _pre_dict(self):
43+
return
44+
3045

3146
@dataclass
32-
class CallToolResult(extism.Json):
47+
class CallToolResult(JSONWizard):
3348
content: List[Content]
3449

3550
# Whether the tool call ended in an error.
3651
#
3752
# If not set, this is assumed to be false (the call was successful).
38-
isError: bool
53+
isError: Optional[bool] = skip_if_field(IS(None), default=None)
54+
55+
@classmethod
56+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
57+
return o
58+
59+
def _pre_dict(self):
60+
return
3961

4062

4163
@dataclass
42-
class Content(extism.Json):
43-
annotations: TextAnnotation
64+
class Content(JSONWizard):
65+
type: ContentType
66+
67+
annotations: Optional[TextAnnotation] = skip_if_field(IS(None), default=None)
4468

4569
# The base64-encoded image data.
46-
data: str
70+
data: Optional[str] = skip_if_field(IS(None), default=None)
4771

4872
# The MIME type of the image. Different providers may support different image types.
49-
mimeType: str
73+
mimeType: Optional[str] = skip_if_field(IS(None), default=None)
5074

5175
# The text content of the message.
52-
text: str
76+
text: Optional[str] = skip_if_field(IS(None), default=None)
5377

54-
type: ContentType
78+
@classmethod
79+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
80+
return o
81+
82+
def _pre_dict(self):
83+
return
5584

5685

5786
class ContentType(Enum):
@@ -61,52 +90,80 @@ class ContentType(Enum):
6190

6291

6392
@dataclass
64-
class ListToolsResult(extism.Json):
93+
class ListToolsResult(JSONWizard):
6594
# The list of ToolDescription objects provided by this servlet.
6695
tools: List[ToolDescription]
6796

97+
@classmethod
98+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
99+
return o
100+
101+
def _pre_dict(self):
102+
return
68103

69-
@dataclass
70-
class Params(extism.Json):
71-
arguments: dict
72104

105+
@dataclass
106+
class Params(JSONWizard):
73107
name: str
74108

109+
arguments: Optional[dict] = skip_if_field(IS(None), default=None)
110+
111+
@classmethod
112+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
113+
return o
114+
115+
def _pre_dict(self):
116+
return
117+
75118

76119
class Role(Enum):
77120
Assistant = "assistant"
78121
User = "user"
79122

80123

81124
@dataclass
82-
class TextAnnotation(extism.Json):
125+
class TextAnnotation(JSONWizard):
83126
# Describes who the intended customer of this object or data is.
84127
#
85128
# It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
86-
audience: List[Role]
129+
audience: Optional[List[Role]] = skip_if_field(IS(None), default=None)
87130

88131
# Describes how important this data is for operating the server.
89132
#
90133
# A value of 1 means "most important," and indicates that the data is
91134
# effectively required, while 0 means "least important," and indicates that
92135
# the data is entirely optional.
93-
priority: float
136+
priority: Optional[float] = skip_if_field(IS(None), default=None)
94137

138+
@classmethod
139+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
140+
return o
95141

96-
@dataclass
97-
class TextResourceContents(extism.Json):
98-
# The MIME type of this resource, if known.
99-
mimeType: str
142+
def _pre_dict(self):
143+
return
100144

145+
146+
@dataclass
147+
class TextResourceContents(JSONWizard):
101148
# The text of the item. This must only be set if the item can actually be represented as text (not binary data).
102149
text: str
103150

104151
# The URI of this resource.
105152
uri: str
106153

154+
# The MIME type of this resource, if known.
155+
mimeType: Optional[str] = skip_if_field(IS(None), default=None)
156+
157+
@classmethod
158+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
159+
return o
160+
161+
def _pre_dict(self):
162+
return
163+
107164

108165
@dataclass
109-
class ToolDescription(extism.Json):
166+
class ToolDescription(JSONWizard):
110167
# A description of the tool
111168
description: str
112169

@@ -115,3 +172,10 @@ class ToolDescription(extism.Json):
115172

116173
# The name of the tool. It should match the plugin / binding name.
117174
name: str
175+
176+
@classmethod
177+
def _pre_from_dict(cls, o: JSONObject) -> JSONObject:
178+
return o
179+
180+
def _pre_dict(self):
181+
return

servlets/obsidian/plugin/plugin.py

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from datetime import datetime # noqa: F401
33
import extism # noqa: F401 # pyright: ignore
44
from urllib.parse import urlencode
5+
import urllib.parse
6+
import json
57

68
from pdk_types import (
79
BlobResourceContents,
@@ -63,7 +65,7 @@ def search(self, query: str, context_length: int = 100):
6365
def append_content(self, path, content):
6466
return self.post(f"/vault/{path}", content, {'Content-Type': 'text/markdown'})
6567

66-
def patch_content(filepath, operation, target_type, target, content):
68+
def patch_content(self, path, operation, target_type, target, content):
6769
headers = {
6870
'Content-Type': 'text/markdown',
6971
'Operation': operation,
@@ -76,7 +78,6 @@ def complex_search(self, query):
7678
headers = {
7779
'Content-Type': 'application/vnd.olrapi.jsonlogic+json',
7880
}
79-
query_string = urlencode(params)
8081
return self.post(f"/search/", json.dumps(query), headers)
8182

8283
def errorReturn(message):
@@ -93,60 +94,56 @@ def errorReturn(message):
9394
isError=True,
9495
)
9596

96-
9797
# Called when the tool is invoked.
9898
# If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
99-
def call(input) -> CallToolResult:
100-
try:
101-
fname = input['params']['name']
102-
except:
103-
raise Exception("params name must be provided")
99+
def call(input: CallToolRequest) -> CallToolResult:
100+
fname = input.params.name
104101
obsidian = Obsidian()
105102
match fname:
106103
case "list_files_in_vault":
107104
contentText = obsidian.list_files_in_vault()
108105
case "list_files_in_dir":
109106
try:
110-
dirpath = input['params']['arguments']['dirpath']
111-
except:
107+
dirpath = input.params.arguments['dirpath']
108+
contentText = obsidian.list_files_in_dir(dirpath)
109+
except (KeyError, TypeError):
112110
return errorReturn("Argument dirpath not provided")
113-
contentText = obsidian.list_files_in_dir(dirpath)
114111
case "get_file_contents":
115112
try:
116-
filepath = input['params']['arguments']['filepath']
117-
except:
113+
filepath = input.params.arguments['filepath']
114+
contentText = obsidian.get_file_contents(filepath)
115+
except (KeyError, TypeError):
118116
return errorReturn("Argument filepath not provided")
119-
contentText = obsidian.get_file_contents(filepath)
120117
case "simple_search":
121118
try:
122-
query = input['params']['arguments']['query']
123-
except:
119+
query = input.params.arguments['query']
120+
context_length = input.params.arguments.get('context_length')
121+
contentText = obsidian.search(query, context_length)
122+
except (KeyError, TypeError):
124123
return errorReturn("Argument query not provided")
125-
context_length = input['params']['arguments'].get('context_length')
126-
contentText = obsidian.search(query, context_length)
127124
case "append_content":
128125
try:
129-
filepath = input['params']['arguments']['filepath']
130-
content = input['params']['arguments']['content']
131-
except:
126+
filepath = input.params.arguments['filepath']
127+
content = input.params.arguments['content']
128+
contentText = obsidian.append_content(filepath, content)
129+
except (KeyError, TypeError):
132130
return errorReturn("Argument filepath or content not provided")
133-
contentText = obsidian.append_content(filepath, content)
134131
case "patch_content":
135132
try:
136-
filepath = input['params']['arguments']['filepath']
137-
operation = input['params']['arguments']['operation']
138-
target_type = input['params']['arguments']['target_type']
139-
target = input['params']['arguments']['target']
140-
content = input['params']['arguments']['content']
141-
except:
133+
filepath = input.params.arguments['filepath']
134+
operation = input.params.arguments['operation']
135+
target_type = input.params.arguments['target_type']
136+
target = input.params.arguments['target']
137+
content = input.params.arguments['content']
138+
contentText = obsidian.patch_content(filepath, operation, target_type, target, content)
139+
except (KeyError, TypeError):
142140
return errorReturn("Arguments missing")
143-
contentText = obsidian.patch_content(filepath, operation, target_type, target, content)
144141
case "complex_search":
145142
try:
146-
query = input['params']['arguments']['query']
147-
except:
143+
query = input.params.arguments['query']
144+
contentText = obsidian.complex_search(query)
145+
except (KeyError, TypeError):
148146
return errorReturn("Argument query not provided")
149-
contentText = obsidian.complex_search(query)
150147
case _:
151148
return errorReturn(f"Unknown tool {fname}")
152149
return CallToolResult(
@@ -159,7 +156,6 @@ def call(input) -> CallToolResult:
159156
data=None,
160157
)
161158
],
162-
isError=False,
163159
)
164160

165161

servlets/obsidian/prepare.sh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,3 @@ if ! command_exists extism-py; then
4040
sleep 2
4141
exit 1
4242
fi
43-
44-

servlets/obsidian/pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ name = "obsidian"
33
version = "0.1.0"
44
description = "Add your description here"
55
readme = "README.md"
6-
requires-python = ">=3.12"
7-
dependencies = []
6+
requires-python = ">=3.13,<3.14"
7+
dependencies = [
8+
"dataclass-wizard>=0.33.0,<0.34.0"
9+
]
810

911
[tool.uv]
1012
dev-dependencies = [

0 commit comments

Comments
 (0)