-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprompts.lua
More file actions
241 lines (210 loc) · 8.3 KB
/
prompts.lua
File metadata and controls
241 lines (210 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
-- MCP Prompt Discovery & Dispatch
-- Discovers prompts from Wippy registry (meta mcp.prompt = true)
-- Handles prompts/list and prompts/get
-- Supports static prompts (messages in meta), dynamic prompts (function handler),
-- template inheritance (extend), and {{argument}} substitution
-- Entry kind: library.lua
local registry = require("registry")
local funcs = require("funcs")
local jsonrpc = require("jsonrpc")
---------------------------------------------------------------------------
-- Prompt discovery from registry
---------------------------------------------------------------------------
local function discover(scope)
local entries, err = registry.find({kind = "function.lua"})
if err then
return nil, err
end
local prompts = {}
for _, entry in ipairs(entries) do
local meta = entry.meta
if meta and meta["mcp.prompt"] == true then
-- Scope filter: scoped prompts only visible on matching endpoints
if meta["mcp.scope"] and meta["mcp.scope"] ~= scope then
-- skip: prompt has a scope that doesn't match this endpoint
else
local name = meta["mcp.prompt.name"] or entry.id
prompts[name] = {
entry_id = entry.id,
name = name,
description = meta["mcp.prompt.description"],
prompt_type = meta["mcp.prompt.type"] or "prompt",
tags = meta["mcp.prompt.tags"],
arguments = meta["mcp.prompt.arguments"],
messages = meta["mcp.prompt.messages"],
extend = meta["mcp.prompt.extend"],
}
end
end
end
return prompts, nil
end
---------------------------------------------------------------------------
-- Argument substitution: replace {{name}} placeholders in text
---------------------------------------------------------------------------
local function substitute(text, arguments)
if not text or not arguments then
return text
end
local result = text
for key, value in pairs(arguments) do
result = string.gsub(result, "{{" .. key .. "}}", tostring(value))
end
return result
end
---------------------------------------------------------------------------
-- Resolve template inheritance (extend chain)
---------------------------------------------------------------------------
local function resolve_messages(prompt, all_prompts, arguments)
local messages = {}
-- 1. Resolve extended templates first (prepend their messages)
if prompt.extend then
for _, ext in ipairs(prompt.extend) do
local parent = all_prompts[ext.id]
if parent then
-- Merge extension arguments with caller arguments
local merged_args = {}
if ext.arguments then
for k, v in pairs(ext.arguments) do
-- Extension arguments may contain {{placeholders}} too
merged_args[k] = substitute(tostring(v), arguments)
end
end
-- Also pass through caller arguments for any remaining placeholders
if arguments then
for k, v in pairs(arguments) do
if not merged_args[k] then
merged_args[k] = v
end
end
end
-- Recursively resolve parent (supports multi-level inheritance)
local parent_msgs = resolve_messages(parent, all_prompts, merged_args)
for _, msg in ipairs(parent_msgs) do
table.insert(messages, msg)
end
end
end
end
-- 2. Append this prompt's own messages
if prompt.messages then
for _, msg in ipairs(prompt.messages) do
local content_text = msg.content
if type(content_text) == "table" and content_text.text then
content_text = content_text.text
end
table.insert(messages, {
role = msg.role or "user",
content = {
type = "text",
text = substitute(tostring(content_text), arguments)
}
})
end
end
return messages
end
---------------------------------------------------------------------------
-- prompts/list handler
---------------------------------------------------------------------------
local function handle_list(id, params, scope)
local prompts, err = discover(scope)
if err then
return jsonrpc.internal_error(id, "Failed to discover prompts: " .. tostring(err))
end
local prompt_list = {}
for _, prompt in pairs(prompts) do
-- Only list prompts, not templates
if prompt.prompt_type ~= "template" then
local entry = { name = prompt.name }
if prompt.description then
entry.description = prompt.description
end
-- Convert schema-style arguments to MCP PromptArgument format
if prompt.arguments then
local args = {}
for _, arg in ipairs(prompt.arguments) do
local a = { name = arg.name }
if arg.description then
a.description = arg.description
end
if arg.required then
a.required = arg.required
end
table.insert(args, a)
end
entry.arguments = args
end
table.insert(prompt_list, entry)
end
end
return jsonrpc.encode_response(id, {prompts = prompt_list})
end
---------------------------------------------------------------------------
-- prompts/get handler
---------------------------------------------------------------------------
local function handle_get(id, params, scope)
local prompt_name = params.name
local arguments = params.arguments or {}
if not prompt_name or prompt_name == "" then
return jsonrpc.invalid_params(id, "Missing prompt name")
end
local prompts, err = discover(scope)
if err then
return jsonrpc.internal_error(id, "Failed to discover prompts: " .. tostring(err))
end
local prompt = prompts[prompt_name]
if not prompt then
return jsonrpc.invalid_params(id, "Unknown prompt: " .. prompt_name)
end
-- Templates cannot be retrieved directly
if prompt.prompt_type == "template" then
return jsonrpc.invalid_params(id, "Cannot get template directly: " .. prompt_name)
end
-- Check if this is a dynamic prompt (has a function handler without static messages)
local messages
if not prompt.messages and not prompt.extend then
-- Dynamic prompt: call the function handler
local result, call_err = funcs.call(prompt.entry_id, arguments)
if call_err then
return jsonrpc.internal_error(id, "Prompt handler error: " .. tostring(call_err))
end
if type(result) == "table" and result.messages then
messages = result.messages
elseif type(result) == "string" then
messages = {
{role = "user", content = {type = "text", text = result}}
}
else
messages = {}
end
else
-- Static prompt: resolve from meta (with template inheritance)
messages = resolve_messages(prompt, prompts, arguments)
end
local result = {messages = messages}
if prompt.description then
result.description = prompt.description
end
return jsonrpc.encode_response(id, result)
end
---------------------------------------------------------------------------
-- Top-level dispatch for prompt methods
---------------------------------------------------------------------------
local function handle(msg, scope)
if msg.kind ~= "request" then
return nil
end
if msg.method == "prompts/list" then
return handle_list(msg.id, msg.params or {}, scope)
elseif msg.method == "prompts/get" then
return handle_get(msg.id, msg.params or {}, scope)
end
return nil
end
return {
discover = discover,
handle_list = handle_list,
handle_get = handle_get,
handle = handle
}