8000 Prompts by jameslong · Pull Request #13 · jameslong/vancouver · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Prompts #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ vancouver-*.tar
.DS_Store

# VSCode config
launch.json
launch.json
mcp.json
2 changes: 1 addition & 1 deletion lib/vancouver/methods/initialize.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Vancouver.Methods.Initialize do
version = Application.get_env(:vancouver, :version, "1.0.0")

result = %{
"capabilities" => %{"tools" => %{}},
"capabilities" => %{"prompts" => %{}, "tools" => %{}},
"protocolVersion" => "2024-11-05",
"serverInfo" => %{
"name" => name,
Expand Down
49 changes: 49 additions & 0 deletions lib/vancouver/methods/prompts_get.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Vancouver.Methods.PromptsGet do
@moduledoc false

import Vancouver.Method
alias Vancouver.JsonRpc2

def run(%Plug.Conn{} = conn, prompts) do
request = conn.body_params
name = request["params"]["name"]
arguments = request["params"]["arguments"]

with {:ok, prompt} <- get_prompt(prompts, name),
:ok <- validate_arguments(prompt, arguments) do
prompt.run(conn, arguments)
else
{:error, :not_found} -> prompt_not_found(conn, request)
{:error, {:invalid_params, reason}} -> invalid_params(conn, request, reason)
end
end

defp get_prompt(prompts, name) do
case Enum.find(prompts, &(&1.name() == name)) do
nil -> {:error, :not_found}
prompt -> {:ok, prompt}
end
end

defp validate_arguments(prompt, arguments) do
case JsonRpc2.validate_schema(prompt.input_schema(), arguments) do
:ok -> :ok
{:error, reason} -> {:error, {:invalid_params, reason}}
end
end

defp prompt_not_found(conn, request) do
data = %{original_request: request}
response = JsonRpc2.error_response(:method_not_found, request["id"], data)

send_json(conn, response)
end

defp invalid_params(conn, request, reason) do
request_id = request["id"]
data = %{error: reason, original_request: request}
response = JsonRpc2.error_response(:invalid_params, request_id, data)

send_json(conn, response)
end
end
14 changes: 14 additions & 0 deletions lib/vancouver/methods/prompts_list.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule Vancouver.Methods.PromptsList do
@moduledoc false

import Vancouver.Method
alias Vancouver.JsonRpc2

def run(%Plug.Conn{} = conn, prompts) do
request = conn.body_params
prompt_definitions = Enum.map(prompts, & &1.definition())
response = JsonRpc2.success_response(request["id"], %{prompts: prompt_definitions})

send_json(conn, response)
end
end
3 changes: 3 additions & 0 deletions lib/vancouver/plugs/dispatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ defmodule Vancouver.Plugs.Dispatch do
def call(%Plug.Conn{} = conn, _opts) do
request = conn.body_params
method = request["method"]
prompts = conn.assigns[:vancouver][:prompts] || []
tools = conn.assigns[:vancouver][:tools] || []

case method do
"initialize" -> Methods.Initialize.run(conn)
"notifications/initialized" -> Methods.Initialized.run(conn)
"ping" -> Methods.Ping.run(conn)
"prompts/list" -> Methods.PromptsList.run(conn, prompts)
"prompts/get" -> Methods.PromptsGet.run(conn, prompts)
"tools/list" -> Methods.ToolsList.run(conn, tools)
"tools/call" -> Methods.ToolsCall.run(conn, tools)
_ -> Methods.Unknown.run(conn)
Expand Down
214 changes: 214 additions & 0 deletions lib/vancouver/prompt.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
defmodule Vancouver.Prompt do
@moduledoc """
Prompts provide LLMs with autogenerated user/assistant prompts.

You can implement a prompt as follows:

defmodule MyApp.Prompts.CodeReview do
use Vancouver.Prompt

def name, do: "code_review"
def description, do: "Asks the LLM to analyze code quality and suggest improvements"

def arguments do
[
%{
"name" => "code",
"description" => "The code to review",
"required" => true
}
]
end

def run(conn, %{"code" => code}) do
send_text(conn, "Please review this code: \#{code}")
end
end

## Sending responses

Prompts provide helper functions to send valid MCP responses:

- `send_audio/4` - sends an audio response
- `send_image/4` - sends an image response
- `send_text/3` - sends a text response
"""

alias Vancouver.JsonRpc2
alias Vancouver.Method

@type role :: :user | :assistant

@doc """
Unique identifier for the prompt.
"""
@callback name() :: String.t()

@doc """
Human readable description of what the prompt does.
"""
@callback description() :: String.t()

@doc """
Schema defining the prompt's arguments.
"""
@callback arguments() :: [map()]

@doc """
Execute the prompt with the given arguments.
"""
@callback run(conn :: Plug.Conn.t(), params :: map()) :: Plug.Conn.t()

defmacro __using__(_opts) do
quote do
@behaviour Vancouver.Prompt
import Vancouver.Prompt

def definition do
%{
"name" => name(),
"description" => description(),
"arguments" => arguments()
}
end

def input_schema do
arguments = arguments()

properties =
arguments
|> Enum.map(&{&1["name"], %{"type" => "string", "description" => &1["description"]}})
|> Enum.into(%{})

required =
arguments
|> Enum.filter(& &1["required"])
|> Enum.map(& &1["name"])

%{
"type" => "object",
"properties" => properties,
"required" => required
}
end
end
end

@doc """
Sends audio response.

Accepts a `:role` option, which can be either `:user` or `:assistant`, defaulting to `:user`.

## Examples

iex> send_audio(conn, "base64-encoded-audio-data", "audio/wav")

iex> send_audio(conn, "base64-encoded-audio-data", "audio/wav", role: :user)

iex> send_audio(conn, "base64-encoded-audio-data", "audio/wav", role: :assistant)

"""
@spec send_audio(Plug.Conn.t(), binary(), binary(), Keyword.t()) :: Plug.Conn.t()
def send_audio(%Plug.Conn{} = conn, base64_data, mime_type, opts \\ [])
when is_binary(base64_data) and is_binary(mime_type) do
role = get_role(opts)

result = %{
"messages" => [
%{
"role" => role,
"content" => %{
"type" => "audio",
"data" => base64_data,
"mimeType" => mime_type
}
}
]
}

send_success(conn, result)
end

@doc """
Sends image response.

Accepts a `:role` option, which can be either `:user` or `:assistant`, defaulting to `:user`.

## Examples

iex> send_image(conn, "base64-encoded-data", "image/png")

iex> send_image(conn, "base64-encoded-data", "image/png", role: :user)

iex> send_image(conn, "base64-encoded-data", "image/png", role: :assistant)

"""
@spec send_image(Plug.Conn.t(), binary(), binary(), Keyword.t()) :: Plug.Conn.t()
def send_image(%Plug.Conn{} = conn, base64_data, mime_type, opts \\ [])
when is_binary(base64_data) and is_binary(mime_type) do
role = get_role(opts)

result = %{
"messages" => [
%{
"role" => role,
"content" => %{
"type" => "image",
"data" => base64_data,
"mimeType" => mime_type
}
}
]
}

send_success(conn, result)
end

@doc """
Sends text response.

Accepts a `:role` option, which can be either `:user` or `:assistant`, defaulting to `:user`.

## Examples

iex> send_text(conn, "hello")

iex> send_text(conn, "hello", role: :user)

iex> send_text(conn, "hello", role: :assistant)

"""
@spec send_text(Plug.Conn.t(), binary(), Keyword.t()) :: Plug.Conn.t()
def send_text(%Plug.Conn{} = conn, text, opts \\ []) when is_binary(text) do
role = get_role(opts)

result = %{
"messages" => [
%{
"role" => role,
"content" => %{
"type" => "text",
"text" => text
}
}
]
}

send_success(conn, result)
end

defp get_role(opts) do
case Keyword.get(opts, :role, :user) do
:user -> "user"
:assistant -> "assistant"
_ -> raise ArgumentError, "Invalid role: #{inspect(opts[:role])}"
end
end

defp send_success(%Plug.Conn{} = conn, result) do
request_id = conn.body_params["id"]
response = JsonRpc2.success_response(request_id, result)

Method.send_json(conn, response)
end
end
6 changes: 2 additions & 4 deletions lib/vancouver/tool.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ defmodule Vancouver.Tool do
Tools provide helper functions to send valid MCP responses:

- `send_json/2` - sends a JSON response
- `send_audio/3` - sends an audio response
- `send_image/3` - sends an image response
- `send_text/2` - sends a text response
- `send_error/2` - sends an error response
"""
Expand Down Expand Up @@ -72,10 +74,6 @@ defmodule Vancouver.Tool do
"inputSchema" => input_schema()
}
end

def validate_arguments(arguments) do
Vancouver.JsonRpc2.validate_schema(input_schema(), arguments)
end
end
end

Expand Down
17 changes: 17 additions & 0 deletions test/vancouver/methods/prompts_list_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Vancouver.Methods.PromptsListTest do
use ExUnit.Case, async: true

import Vancouver.TestHelpers
alias Vancouver.Methods.PromptsList

describe "run/2" do
test "returns empty prompts list" do
conn =
tools_list_request()
|> request_conn()
|> PromptsList.run([])

assert_success(conn)
end
end
end
0