lictor-mcp 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 29c5c04710c3fecad7fd15dcd98cf622587246be8cb12fb00a72b8b6a0b78161
4
+ data.tar.gz: 9ca331224ec30c255338d4f46927bbaa6b85387af5cee178e3bef919d848541a
5
+ SHA512:
6
+ metadata.gz: 4d5642b38805cd2d4932034e7b5424c7c05e87d977816fa17bfb256d7230af2cd7fdb8ff32fe7f71df53ada87a6d3285427a448b1e2c4ffc7598f5fd82ef04ba
7
+ data.tar.gz: 14cb858b0170bc2fbc714affb40568f3d1b35cef706041503eeb3b0c37958a954ca2e037e289d0f253a661bf1613e04096663e0f619b8bfe371c37a8a5de4166
data/exe/lictor-mcp ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ unless defined?(Bundler)
5
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__)
6
+ require "bundler/setup"
7
+ end
8
+
9
+ require "lictor-mcp"
10
+
11
+ transport = MCP::Server::Transports::StdioTransport.new(LictorMcp::Server.build)
12
+ transport.open
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ unless defined?(Bundler)
5
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__)
6
+ require "bundler/setup"
7
+ end
8
+
9
+ require "lictor-mcp"
10
+
11
+ transport = MCP::Server::Transports::StdioTransport.new(LictorMcp::Server.build)
12
+ transport.open
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require "lictor"
5
+
6
+ module LictorMcp
7
+ # The MCP surface of the orchestrator: the LLM harness's hands.
8
+ # Every tool runs server-side - the pack armor, the corpus guard,
9
+ # the trail (0600). The agent proposes; augure disposes; lictor
10
+ # executes; the trail records everything.
11
+ module ToolHelpers
12
+ module_function
13
+
14
+ def respond(payload)
15
+ ::MCP::Tool::Response.new([{type: "text", text: ::JSON.generate(payload)}])
16
+ end
17
+
18
+ def error(message)
19
+ ::MCP::Tool::Response.new([{type: "text", text: ::JSON.generate(error: message)}])
20
+ end
21
+
22
+ def trail
23
+ ::Lictor::Trail.new(::Lictor::Config.load[:trail] || "lictor-mcp-trail.jsonl")
24
+ end
25
+ end
26
+
27
+ class Server
28
+ include ToolHelpers
29
+
30
+ TOOLS = [
31
+ ScanTarget = Class.new(::MCP::Tool) do
32
+ description "Run nmap against a target and return the target inventory."
33
+ input_schema(properties: {
34
+ host: {type: "string"}
35
+ }, required: %w[host])
36
+
37
+ class << self
38
+ include ::LictorMcp::ToolHelpers
39
+
40
+ def call(host:, server_context: nil)
41
+ targets = nil
42
+ Dir.mktmpdir do |d|
43
+ targets = ::Lictor::Scanner.run(host, out: File.join(d, "s.xml"))
44
+ end
45
+ trail.record(run: SecureRandom.uuid, type: "scan", target: host)
46
+ respond(targets: targets, candidates: targets.map { |t| "#{t[:host]}:#{t[:port]}" })
47
+ rescue ::Lictor::ScanError => e
48
+ error(e.message)
49
+ end
50
+ end
51
+ end,
52
+
53
+ PlanTechnique = Class.new(::MCP::Tool) do
54
+ description "Decide on facts (or a binary - profiled automatically) and return the " \
55
+ "auditable decision with rule provenance. Augure disposes: the agent " \
56
+ "augure decides - you cannot pick an unverified technique."
57
+ input_schema(properties: {
58
+ target: {type: "string", description: "binary path or .facts path"}
59
+ }, required: %w[target])
60
+
61
+ class << self
62
+ include ::LictorMcp::ToolHelpers
63
+
64
+ def call(target:, server_context: nil)
65
+ facts = ::Lictor::Profiler.elf?(target) ? ::Lictor::Profiler.run(target) : File.read(target)
66
+ packs = ::Lictor::TechniquePacks.load(::Lictor::Config.load[:packs] || "packs")
67
+ decision = ::Lictor::Handoff.plan(facts: facts, priors: ::Lictor::Priors.from_trail(trail), packs: packs)
68
+ trail.record(run: SecureRandom.uuid, type: "plan", selected: decision[:selected],
69
+ applicable: decision[:applicable])
70
+ respond(decision)
71
+ rescue ::Augure::MalformedFact, ::Augure::UnknownPredicate, ::Lictor::ProfilerError => e
72
+ error(e.message)
73
+ end
74
+ end
75
+ end,
76
+
77
+ ExecuteTechnique = Class.new(::MCP::Tool) do
78
+ description "Execute the decided technique against the target. " \
79
+ "The executor discovers exploits from the operator's repos; it never " \
80
+ "invents one. The trail records what actually executed."
81
+ input_schema(properties: {
82
+ target: {type: "string"}, technique: {type: "string"},
83
+ exploit_name: {type: "string", description: "from your repos (ronin-exploits list)"},
84
+ payload_file: {type: "string", description: "a pre-built payload from lictor build"}
85
+ }, required: %w[target technique exploit_name])
86
+
87
+ class << self
88
+ include ::LictorMcp::ToolHelpers
89
+
90
+ def call(target:, technique:, exploit_name:, payload_file: nil, server_context: nil)
91
+ executor = ::Lictor::Executor.resolve(:ronin)
92
+ result = executor.execute(target: target, technique: technique, recipe: {console: ""},
93
+ exploit_name: exploit_name, payload: payload_file)
94
+ trail.record(run: SecureRandom.uuid, type: "outcome", technique: technique,
95
+ success: result[:success] == true, executed: result[:executed],
96
+ target: target)
97
+ respond(result)
98
+ rescue ::Lictor::ExecutorError => e
99
+ error(e.message)
100
+ end
101
+ end
102
+ end,
103
+
104
+ ValidatePack = Class.new(::MCP::Tool) do
105
+ description "Validate a technique pack through the full armor (schema, stratification, " \
106
+ "corpus guard). Installing needs the operator's " \
107
+ "interactive confirmation - validation alone is safe."
108
+ input_schema(properties: {pack_path: {type: "string"}}, required: %w[pack_path])
109
+
110
+ class << self
111
+ include ::LictorMcp::ToolHelpers
112
+
113
+ def call(pack_path:, server_context: nil)
114
+ pack = ::Augure::PackLoader.load_file(pack_path)
115
+ ::Augure::PackLoader.corpus_guard([pack])
116
+ respond(valid: true, technique: pack.technique, author: pack.author, source: pack.source)
117
+ rescue ::Augure::PackError => e
118
+ error("pack refused: #{e.message}")
119
+ end
120
+ end
121
+ end,
122
+ BuildPayload = Class.new(::MCP::Tool) do
123
+ description "Assemble the payload for the decided technique: the offset is " \
124
+ "MEASURED (static frame read, the debugger as fallback), the return " \
125
+ "address comes from the facts, the shellcode is assembly source. " \
126
+ "Writes a payload file and trails the build."
127
+ input_schema(properties: {
128
+ binary: {type: "string", description: "the acquired local ELF copy"},
129
+ facts_path: {type: "string", description: "the .facts file"},
130
+ technique: {type: "string", description: "defaults to the plan's selection"},
131
+ payload_file: {type: "string", description: "custom shellcode/payload bytes"}
132
+ }, required: %w[binary facts_path])
133
+
134
+ class << self
135
+ include ::LictorMcp::ToolHelpers
136
+
137
+ def call(binary:, facts_path:, technique: nil, payload_file: nil, server_context: nil)
138
+ facts = ::File.read(facts_path)
139
+ packs = ::Lictor::TechniquePacks.load(::Lictor::Config.load[:packs] || "packs")
140
+ technique ||= ::Lictor::Handoff.plan(facts: facts, packs: packs)[:selected]
141
+ raise ::Lictor::PayloadBuilder::BuildError, "no technique selected - plan decided nothing" unless technique
142
+
143
+ result = ::Lictor::PayloadBuilder.build(binary: binary, facts: facts,
144
+ technique: technique, out: ::File.join(::Dir.tmpdir, "lictor-payload-#{::Process.pid}.bin"),
145
+ payload_file: payload_file)
146
+ trail.record(run: ::SecureRandom.uuid, type: "build", technique: technique,
147
+ offset: result[:offset], offset_source: result[:offset_source],
148
+ ret: result[:ret_source], size: result[:size], out: result[:path])
149
+ respond(result)
150
+ rescue ::Lictor::PayloadBuilder::BuildError, ::Augure::MalformedFact, ::Augure::UnknownPredicate => e
151
+ error(e.message)
152
+ end
153
+ end
154
+ end,
155
+
156
+ ScaffoldExploit = Class.new(::MCP::Tool) do
157
+ description "Draft a ronin exploit for the decided technique, carrying the measured " \
158
+ "constants: the file lands in the staging directory and the OPERATOR " \
159
+ "reviews and commits it. The exploit NAME follows the discovery convention."
160
+ input_schema(properties: {
161
+ binary: {type: "string"},
162
+ facts_path: {type: "string"},
163
+ technique: {type: "string"},
164
+ staging_dir: {type: "string", description: "where the draft lands (operator-reviewed)"}
165
+ }, required: %w[binary facts_path technique staging_dir])
166
+
167
+ class << self
168
+ include ::LictorMcp::ToolHelpers
169
+
170
+ def call(binary:, facts_path:, technique:, staging_dir:, server_context: nil)
171
+ # The scaffold touches no target: the staging directory is
172
+ # where the draft lands, the operator reviews, the trail
173
+ # records.
174
+ facts = ::File.read(facts_path)
175
+ result = ::Lictor::PayloadBuilder.build(binary: binary, facts: facts,
176
+ technique: technique, out: ::File.join(::Dir.tmpdir, "lictor-payload-#{::Process.pid}.bin"))
177
+ dest = ::Lictor::ExploitScaffold.scaffold(technique: technique, constants: result,
178
+ repo_dir: staging_dir, author: "lictor-mcp scaffold (operator review required)")
179
+ trail.record(run: ::SecureRandom.uuid, type: "scaffold", technique: technique,
180
+ offset: result[:offset], ret: result[:ret_source], file: dest)
181
+ respond(scaffolded: dest, review: "the operator reads and commits this file")
182
+ rescue ::Lictor::PayloadBuilder::BuildError, ::Lictor::ExploitScaffold::ScaffoldError,
183
+ ::Augure::MalformedFact, ::Augure::UnknownPredicate => e
184
+ error(e.message)
185
+ end
186
+ end
187
+ end,
188
+
189
+ SuggestFacts = Class.new(::MCP::Tool) do
190
+ description "Submit hypothesis facts on top of an existing fact base; the closed " \
191
+ "schema is the whitelist (no applicable() exists to forge), augure " \
192
+ "re-decides on the merged base, and the delta comes back. The trail " \
193
+ "records who proposed what and what changed."
194
+ input_schema(properties: {
195
+ facts_path: {type: "string", description: "path to the base .facts file"},
196
+ suggested_facts: {type: "string", description: "hypothesis facts, one predicate(\"atom\"). per line"}
197
+ }, required: %w[facts_path suggested_facts])
198
+
199
+ class << self
200
+ include ::LictorMcp::ToolHelpers
201
+
202
+ def call(facts_path:, suggested_facts:, server_context: nil)
203
+ result = ::Lictor::Suggestion.apply(base_facts: File.read(facts_path),
204
+ suggested: suggested_facts, trail: trail, target: facts_path)
205
+ respond(target: facts_path, delta: {
206
+ added: result[:added],
207
+ applicable_before: result[:applicable_before],
208
+ applicable_after: result[:applicable_after],
209
+ selected_before: result[:selected_before],
210
+ selected_after: result[:selected_after]
211
+ })
212
+ rescue ::Augure::MalformedFact => e
213
+ # the parse error embeds the offending line: a file named by
214
+ # the caller must not leak its content through the error
215
+ error(e.message.sub(/line \d+: malformed fact .*/, "line ?"))
216
+ rescue ::Augure::UnknownPredicate => e
217
+ error(e.message)
218
+ end
219
+ end
220
+ end
221
+ ].freeze
222
+
223
+ def self.build
224
+ ::MCP::Server.new(name: "lictor-mcp", version: ::LictorMcp::VERSION, tools: TOOLS).tap do |server|
225
+ server.instructions = "lictor orchestrates the augure decision layer. The operator is " \
226
+ "the authorization; the trail is the record."
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LictorMcp
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+ require "lictor"
5
+ require_relative "lictor-mcp/version"
6
+ require_relative "lictor-mcp/server"
7
+
8
+ module LictorMcp
9
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lictor-mcp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gregory Epp
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lictor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: mcp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.6'
40
+ description: 'MCP tools over the lictor orchestrator: scan, plan, execute, validate_pack,
41
+ build_payload, scaffold_exploit, suggest_facts - validated SERVER-side (the pack
42
+ armor, the corpus guard, the trail). The LLM proposes; augure disposes; lictor executes;
43
+ the trail records.'
44
+ email:
45
+ - log0u7@users.noreply.github.com
46
+ executables:
47
+ - lictor-mcp
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - "/home/runner/work/lictor/lictor/mcp_server/exe/lictor-mcp"
52
+ - "/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp.rb"
53
+ - "/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp/server.rb"
54
+ - "/home/runner/work/lictor/lictor/mcp_server/lib/lictor-mcp/version.rb"
55
+ - exe/lictor-mcp
56
+ homepage: https://github.com/log0u7/lictor
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ homepage_uri: https://github.com/log0u7/lictor
61
+ source_code_uri: https://github.com/log0u7/lictor
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '3.4'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 3.6.9
77
+ specification_version: 4
78
+ summary: MCP surface for the lictor orchestrator - the LLM harness's hands, server-side
79
+ validated.
80
+ test_files: []