linear-toon-mcp 0.0.1 → 0.2.0
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.
- checksums.yaml +4 -4
- data/README.md +9 -13
- data/lib/linear_toon_mcp/client.rb +62 -0
- data/lib/linear_toon_mcp/tools/get_issue.rb +69 -0
- data/lib/linear_toon_mcp/tools/list_issues.rb +173 -0
- data/lib/linear_toon_mcp/version.rb +1 -1
- data/lib/linear_toon_mcp.rb +12 -3
- metadata +18 -2
- data/lib/linear_toon_mcp/tools/echo.rb +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50a8ce17d061de79481c874e5cd0bd06cb2ab7656101798fb9e312ba4e890a4f
|
|
4
|
+
data.tar.gz: 986cd4e9927a53c969f9a2359013a626b5dfe0b9de2e201b5a5e52cb5faa4635
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4755fd71c9c8cf68259264351fd3e9cf9cadf6f1af6371377f28c944ae0af3bf67d300173c382d0eba92a3dd132738a5d7e2e6067dba9721f69ae624ae29d15d
|
|
7
|
+
data.tar.gz: 0bd32d72d4fe123c043107de21cb50dbe1387b4ae4b76e48ccb4105a7555ffd068991c756acca755666b201a06484ce83c6bd7a03a7134662f06a3a4bddfe17d
|
data/README.md
CHANGED
|
@@ -36,21 +36,17 @@ Get your API key from [Linear Settings > API](https://linear.app/settings/api).
|
|
|
36
36
|
|
|
37
37
|
### Claude Code
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```json
|
|
42
|
-
{
|
|
43
|
-
"mcpServers": {
|
|
44
|
-
"linear": {
|
|
45
|
-
"command": "linear-toon-mcp",
|
|
46
|
-
"env": {
|
|
47
|
-
"LINEAR_API_KEY": "lin_api_xxxxx"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
39
|
+
```bash
|
|
40
|
+
claude mcp add -e LINEAR_API_KEY=lin_api_xxxxx linear-toon -- linear-toon-mcp
|
|
52
41
|
```
|
|
53
42
|
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
| Tool | Description |
|
|
46
|
+
|------|-------------|
|
|
47
|
+
| `get_issue` | Retrieve a Linear issue by ID or identifier (e.g., `LIN-123`). Returns issue details including title, description, state, assignee, labels, project, and attachments. |
|
|
48
|
+
| `list_issues` | List issues with optional filters (team, assignee, state, label, priority, project, cycle) and cursor-based pagination. Supports name or UUID for most filters. |
|
|
49
|
+
|
|
54
50
|
## Development
|
|
55
51
|
|
|
56
52
|
```bash
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module LinearToonMcp
|
|
8
|
+
# Minimal HTTP client for Linear's GraphQL API.
|
|
9
|
+
class Client
|
|
10
|
+
ENDPOINT = URI("https://api.linear.app/graphql").freeze
|
|
11
|
+
|
|
12
|
+
# @param api_key [String] Linear API key (defaults to +LINEAR_API_KEY+ env var)
|
|
13
|
+
# @raise [ArgumentError] when API key is nil or empty
|
|
14
|
+
def initialize(api_key: ENV["LINEAR_API_KEY"])
|
|
15
|
+
raise ArgumentError, "LINEAR_API_KEY is required" if api_key.nil? || api_key.empty?
|
|
16
|
+
|
|
17
|
+
@api_key = api_key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Execute a GraphQL query against Linear API.
|
|
21
|
+
# @param query_string [String] GraphQL query
|
|
22
|
+
# @param variables [Hash] query variables
|
|
23
|
+
# @return [Hash] the +data+ key from the GraphQL response
|
|
24
|
+
# @raise [Error] on HTTP errors, GraphQL errors, or empty responses
|
|
25
|
+
def query(query_string, variables: {})
|
|
26
|
+
response = post(query_string, variables)
|
|
27
|
+
|
|
28
|
+
body = begin
|
|
29
|
+
JSON.parse(response.body)
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
35
|
+
messages = body&.dig("errors")&.map { |e| e["message"] }&.join("; ")
|
|
36
|
+
raise Error, "HTTP #{response.code}: #{messages || response.body}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
raise Error, "Empty response from Linear API" unless body
|
|
40
|
+
|
|
41
|
+
if body["errors"]&.any?
|
|
42
|
+
messages = body["errors"].map { |e| e["message"] }.join("; ")
|
|
43
|
+
raise Error, "GraphQL error: #{messages}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
body["data"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def post(query_string, variables)
|
|
52
|
+
request = Net::HTTP::Post.new(ENDPOINT)
|
|
53
|
+
request["Content-Type"] = "application/json"
|
|
54
|
+
request["Authorization"] = @api_key
|
|
55
|
+
request.body = JSON.generate(query: query_string, variables:)
|
|
56
|
+
|
|
57
|
+
Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: true) do |http|
|
|
58
|
+
http.request(request)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toon"
|
|
4
|
+
|
|
5
|
+
module LinearToonMcp
|
|
6
|
+
module Tools
|
|
7
|
+
# Fetch a single Linear issue by ID or identifier and return it as TOON.
|
|
8
|
+
# Includes metadata, state, assignee, labels, project, team, and attachments.
|
|
9
|
+
class GetIssue < MCP::Tool
|
|
10
|
+
description "Retrieve a Linear issue by ID"
|
|
11
|
+
|
|
12
|
+
annotations(
|
|
13
|
+
read_only_hint: true,
|
|
14
|
+
destructive_hint: false,
|
|
15
|
+
idempotent_hint: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
properties: {
|
|
20
|
+
id: {type: "string", description: "Issue ID or identifier (e.g., LIN-123)"}
|
|
21
|
+
},
|
|
22
|
+
required: ["id"],
|
|
23
|
+
additionalProperties: false
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
QUERY = <<~GRAPHQL
|
|
27
|
+
query($id: String!) {
|
|
28
|
+
issue(id: $id) {
|
|
29
|
+
id
|
|
30
|
+
identifier
|
|
31
|
+
title
|
|
32
|
+
description
|
|
33
|
+
priority
|
|
34
|
+
priorityLabel
|
|
35
|
+
url
|
|
36
|
+
branchName
|
|
37
|
+
createdAt
|
|
38
|
+
updatedAt
|
|
39
|
+
archivedAt
|
|
40
|
+
completedAt
|
|
41
|
+
dueDate
|
|
42
|
+
state { name }
|
|
43
|
+
assignee { id name }
|
|
44
|
+
creator { id name }
|
|
45
|
+
labels { nodes { name } }
|
|
46
|
+
project { id name }
|
|
47
|
+
team { id name }
|
|
48
|
+
attachments { nodes { id title url } }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
GRAPHQL
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# @param id [String] Linear issue ID or identifier (e.g., "LIN-123")
|
|
55
|
+
# @param server_context [Hash, nil] must contain +:client+ key with a {Client}
|
|
56
|
+
# @return [MCP::Tool::Response] TOON-encoded issue or error
|
|
57
|
+
def call(id:, server_context: nil)
|
|
58
|
+
client = server_context&.dig(:client) or raise Error, "client missing from server_context"
|
|
59
|
+
data = client.query(QUERY, variables: {id:})
|
|
60
|
+
issue = data["issue"] or raise Error, "Issue not found: #{id}"
|
|
61
|
+
text = Toon.encode(issue)
|
|
62
|
+
MCP::Tool::Response.new([{type: "text", text:}])
|
|
63
|
+
rescue Error => e
|
|
64
|
+
MCP::Tool::Response.new([{type: "text", text: e.message}], error: true)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toon"
|
|
4
|
+
|
|
5
|
+
module LinearToonMcp
|
|
6
|
+
module Tools
|
|
7
|
+
# List Linear issues with optional filters and pagination.
|
|
8
|
+
# Returns TOON-encoded array with page info for cursor-based pagination.
|
|
9
|
+
class ListIssues < MCP::Tool
|
|
10
|
+
description "List issues with optional filters and pagination"
|
|
11
|
+
|
|
12
|
+
annotations(
|
|
13
|
+
read_only_hint: true,
|
|
14
|
+
destructive_hint: false,
|
|
15
|
+
idempotent_hint: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
properties: {
|
|
20
|
+
assignee: {type: "string", description: 'User ID, name, email, or "me"'},
|
|
21
|
+
createdAt: {type: "string", description: "Created after: ISO-8601 date/duration (e.g., -P1D)"},
|
|
22
|
+
cursor: {type: "string", description: "Next page cursor"},
|
|
23
|
+
cycle: {type: "string", description: "Cycle name, number, or ID"},
|
|
24
|
+
delegate: {type: "string", description: "Agent name or ID"},
|
|
25
|
+
includeArchived: {type: "boolean", description: "Include archived items (default true)"},
|
|
26
|
+
label: {type: "string", description: "Label name or ID"},
|
|
27
|
+
limit: {type: "integer", description: "Max results (default 50, max 250)"},
|
|
28
|
+
orderBy: {type: "string", description: "createdAt or updatedAt (default updatedAt)", enum: ["createdAt", "updatedAt"]},
|
|
29
|
+
parentId: {type: "string", description: "Parent issue ID"},
|
|
30
|
+
priority: {type: "integer", description: "0=None, 1=Urgent, 2=High, 3=Normal, 4=Low"},
|
|
31
|
+
project: {type: "string", description: "Project name or ID"},
|
|
32
|
+
query: {type: "string", description: "Search issue title or description"},
|
|
33
|
+
state: {type: "string", description: "State name or ID"},
|
|
34
|
+
team: {type: "string", description: "Team name or ID"},
|
|
35
|
+
updatedAt: {type: "string", description: "Updated after: ISO-8601 date/duration (e.g., -P1D)"}
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
QUERY = <<~GRAPHQL
|
|
41
|
+
query($filter: IssueFilter, $first: Int, $after: String, $orderBy: PaginationOrderBy, $includeArchived: Boolean) {
|
|
42
|
+
issues(filter: $filter, first: $first, after: $after, orderBy: $orderBy, includeArchived: $includeArchived) {
|
|
43
|
+
nodes {
|
|
44
|
+
id
|
|
45
|
+
identifier
|
|
46
|
+
title
|
|
47
|
+
priority
|
|
48
|
+
priorityLabel
|
|
49
|
+
url
|
|
50
|
+
createdAt
|
|
51
|
+
updatedAt
|
|
52
|
+
state { name }
|
|
53
|
+
assignee { id name }
|
|
54
|
+
labels { nodes { name } }
|
|
55
|
+
project { id name }
|
|
56
|
+
team { id name }
|
|
57
|
+
}
|
|
58
|
+
pageInfo {
|
|
59
|
+
hasNextPage
|
|
60
|
+
endCursor
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
GRAPHQL
|
|
65
|
+
|
|
66
|
+
# @private
|
|
67
|
+
UUID_RE = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
68
|
+
# @private
|
|
69
|
+
NUMERIC_RE = /\A\d+\z/
|
|
70
|
+
|
|
71
|
+
# standard:disable Naming/VariableName
|
|
72
|
+
class << self
|
|
73
|
+
# @param assignee [String, nil] user ID, name, email, or "me"
|
|
74
|
+
# @param createdAt [String, nil] ISO-8601 date or duration
|
|
75
|
+
# @param cursor [String, nil] pagination cursor
|
|
76
|
+
# @param cycle [String, nil] cycle name, number, or ID
|
|
77
|
+
# @param delegate [String, nil] agent name or ID
|
|
78
|
+
# @param includeArchived [Boolean, nil] include archived issues
|
|
79
|
+
# @param label [String, nil] label name or ID
|
|
80
|
+
# @param limit [Integer, nil] max results (default 50, max 250)
|
|
81
|
+
# @param orderBy [String, nil] "createdAt" or "updatedAt"
|
|
82
|
+
# @param parentId [String, nil] parent issue ID
|
|
83
|
+
# @param priority [Integer, nil] 0-4
|
|
84
|
+
# @param project [String, nil] project name or ID
|
|
85
|
+
# @param query [String, nil] search title or description
|
|
86
|
+
# @param state [String, nil] state name or ID
|
|
87
|
+
# @param team [String, nil] team name or ID
|
|
88
|
+
# @param updatedAt [String, nil] ISO-8601 date or duration
|
|
89
|
+
# @param server_context [Hash, nil] must contain +:client+ key with a {Client}
|
|
90
|
+
# @return [MCP::Tool::Response] TOON-encoded issue list or error
|
|
91
|
+
def call(assignee: nil, createdAt: nil, cursor: nil, cycle: nil,
|
|
92
|
+
delegate: nil, includeArchived: nil, label: nil, limit: nil, orderBy: nil,
|
|
93
|
+
parentId: nil, priority: nil, project: nil, query: nil, state: nil,
|
|
94
|
+
team: nil, updatedAt: nil, server_context: nil)
|
|
95
|
+
client = server_context&.dig(:client) or raise Error, "client missing from server_context"
|
|
96
|
+
|
|
97
|
+
filter = build_filter(
|
|
98
|
+
assignee:, team:, project:, state:, label:,
|
|
99
|
+
priority:, parentId:, cycle:, delegate:,
|
|
100
|
+
query:, createdAt:, updatedAt:
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
variables = {
|
|
104
|
+
first: (limit || 50).clamp(1, 250),
|
|
105
|
+
orderBy: orderBy || "updatedAt",
|
|
106
|
+
includeArchived: includeArchived != false
|
|
107
|
+
}
|
|
108
|
+
variables[:filter] = filter unless filter.empty?
|
|
109
|
+
variables[:after] = cursor if cursor
|
|
110
|
+
|
|
111
|
+
data = client.query(QUERY, variables:)
|
|
112
|
+
issues = data["issues"] or raise Error, "Unexpected response: missing issues field"
|
|
113
|
+
text = Toon.encode(issues)
|
|
114
|
+
MCP::Tool::Response.new([{type: "text", text:}])
|
|
115
|
+
rescue Error => e
|
|
116
|
+
MCP::Tool::Response.new([{type: "text", text: e.message}], error: true)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def build_filter(assignee:, team:, project:, state:, label:,
|
|
122
|
+
priority:, parentId:, cycle:, delegate:,
|
|
123
|
+
query:, createdAt:, updatedAt:)
|
|
124
|
+
filter = {}
|
|
125
|
+
filter[:assignee] = assignee_filter(assignee) if assignee
|
|
126
|
+
filter[:team] = name_or_id(team) if team
|
|
127
|
+
filter[:project] = name_or_id(project) if project
|
|
128
|
+
filter[:state] = name_or_id(state) if state
|
|
129
|
+
filter[:labels] = {some: name_or_id(label)} if label
|
|
130
|
+
filter[:priority] = {eq: priority} if priority
|
|
131
|
+
filter[:parent] = {id: {eq: parentId}} if parentId
|
|
132
|
+
filter[:cycle] = cycle_filter(cycle) if cycle
|
|
133
|
+
filter[:delegate] = name_or_id(delegate) if delegate
|
|
134
|
+
if query
|
|
135
|
+
filter[:or] = [
|
|
136
|
+
{title: {containsIgnoreCase: query}},
|
|
137
|
+
{description: {containsIgnoreCase: query}}
|
|
138
|
+
]
|
|
139
|
+
end
|
|
140
|
+
filter[:createdAt] = {gte: resolve_date(createdAt)} if createdAt
|
|
141
|
+
filter[:updatedAt] = {gte: resolve_date(updatedAt)} if updatedAt
|
|
142
|
+
filter
|
|
143
|
+
end
|
|
144
|
+
# standard:enable Naming/VariableName
|
|
145
|
+
|
|
146
|
+
def assignee_filter(value)
|
|
147
|
+
return {isMe: {eq: true}} if value == "me"
|
|
148
|
+
return {id: {eq: value}} if value.match?(UUID_RE)
|
|
149
|
+
return {email: {eq: value}} if value.include?("@")
|
|
150
|
+
{name: {eqIgnoreCase: value}}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def name_or_id(value)
|
|
154
|
+
value.match?(UUID_RE) ? {id: {eq: value}} : {name: {eqIgnoreCase: value}}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def cycle_filter(value)
|
|
158
|
+
return {id: {eq: value}} if value.match?(UUID_RE)
|
|
159
|
+
return {number: {eq: value.to_i}} if value.match?(NUMERIC_RE)
|
|
160
|
+
{name: {eqIgnoreCase: value}}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def resolve_date(value)
|
|
164
|
+
return value unless value.start_with?("-P")
|
|
165
|
+
match = value.match(/\A-P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?\z/)
|
|
166
|
+
raise Error, "Invalid duration: #{value}" unless match
|
|
167
|
+
days = match.captures.zip([365, 30, 7, 1]).sum { |c, m| (c&.to_i || 0) * m }
|
|
168
|
+
(Time.now.utc - (days * 86_400)).iso8601
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/linear_toon_mcp.rb
CHANGED
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require "mcp"
|
|
4
4
|
require_relative "linear_toon_mcp/version"
|
|
5
|
-
require_relative "linear_toon_mcp/
|
|
5
|
+
require_relative "linear_toon_mcp/client"
|
|
6
|
+
require_relative "linear_toon_mcp/tools/get_issue"
|
|
7
|
+
require_relative "linear_toon_mcp/tools/list_issues"
|
|
6
8
|
|
|
9
|
+
# Token-efficient MCP server for Linear. Wraps Linear's GraphQL API
|
|
10
|
+
# and returns TOON-formatted responses for ~40-60% token savings.
|
|
7
11
|
module LinearToonMcp
|
|
12
|
+
# Raised on Linear API HTTP errors, GraphQL errors, or missing data.
|
|
8
13
|
class Error < StandardError; end
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
# Build a configured MCP::Server with all registered tools.
|
|
16
|
+
# @param client [Client] Linear API client (defaults to new instance from ENV)
|
|
17
|
+
# @return [MCP::Server]
|
|
18
|
+
def self.server(client: Client.new)
|
|
11
19
|
MCP::Server.new(
|
|
12
20
|
name: "linear-toon-mcp",
|
|
13
21
|
version: VERSION,
|
|
14
22
|
description: "Manage Linear issues, projects, and teams",
|
|
15
|
-
tools: [Tools::
|
|
23
|
+
tools: [Tools::GetIssue, Tools::ListIssues],
|
|
24
|
+
server_context: {client:}
|
|
16
25
|
)
|
|
17
26
|
end
|
|
18
27
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: linear-toon-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yevhenii Hurin
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.6'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: openssl
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 3.3.1
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 3.3.1
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: toon-ruby
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -50,7 +64,9 @@ files:
|
|
|
50
64
|
- README.md
|
|
51
65
|
- bin/linear-toon-mcp
|
|
52
66
|
- lib/linear_toon_mcp.rb
|
|
53
|
-
- lib/linear_toon_mcp/
|
|
67
|
+
- lib/linear_toon_mcp/client.rb
|
|
68
|
+
- lib/linear_toon_mcp/tools/get_issue.rb
|
|
69
|
+
- lib/linear_toon_mcp/tools/list_issues.rb
|
|
54
70
|
- lib/linear_toon_mcp/version.rb
|
|
55
71
|
homepage: https://github.com/hoblin/linear-toon-mcp
|
|
56
72
|
licenses:
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LinearToonMcp
|
|
4
|
-
module Tools
|
|
5
|
-
class Echo < MCP::Tool
|
|
6
|
-
description "Accepts text input and returns it as-is"
|
|
7
|
-
|
|
8
|
-
annotations(
|
|
9
|
-
read_only_hint: true,
|
|
10
|
-
destructive_hint: false,
|
|
11
|
-
idempotent_hint: true
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
input_schema(
|
|
15
|
-
properties: {
|
|
16
|
-
text: {type: "string", description: "Text to echo back"}
|
|
17
|
-
},
|
|
18
|
-
required: ["text"],
|
|
19
|
-
additionalProperties: false
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
class << self
|
|
23
|
-
def call(text:, server_context: nil)
|
|
24
|
-
MCP::Tool::Response.new([{type: "text", text:}])
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|