Skip to content

Commit ca1b5fd

Browse files
committed
Add MCP support with two read-only tools
1 parent 92da140 commit ca1b5fd

File tree

18 files changed

+763
-246
lines changed

18 files changed

+763
-246
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ blib
88
pm_to_blib
99
MYMETA.*
1010
Makefile
11+
.gemini

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* Legal risk assessments by lawyers for every pattern match
1515
* Human reviews with approval/rejection workflow, and optional automatic approvals based on risk
1616
* Optional support for machine learning models to classify pattern matches
17+
* MCP support for integration into AI assisted legal review workflows
1718
* REST API for integration into existing source code management systems
1819
* [Open Build Service](https://github.com/openSUSE/openSUSE-release-tools) and [Gitea](https://github.com/openSUSE/cavil-gitea) integration
1920
via bots
@@ -59,7 +60,7 @@ There are currently two example implementations for a companion server applicati
5960
perl-Mojo-Pg perl-Minion perl-File-Unpack perl-Cpanel-JSON-XS \
6061
perl-Spooky-Patterns-XS perl-Mojolicious-Plugin-OAuth2 perl-Mojo-JWT \
6162
perl-BSD-Resource perl-Term-ProgressBar perl-Text-Glob perl-IPC-Run \
62-
perl-Try-Tiny git git-lfs
63+
perl-Try-Tiny perl-MCP git git-lfs
6364
$ npm i
6465
$ npm run build
6566

assets/vue/ApiKeys.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<thead>
2323
<tr>
2424
<th>API Key</th>
25+
<th>Type</th>
2526
<th>Description</th>
2627
<th>Expires</th>
2728
<th></th>
@@ -47,6 +48,7 @@
4748
<span class="real">{{ key.apiKey }}</span>
4849
</span>
4950
</td>
51+
<td>read-only</td>
5052
<td>{{ key.description }}</td>
5153
<td>{{ key.expires }}</td>
5254
<td class="text-center">
@@ -58,7 +60,7 @@
5860
</tbody>
5961
<tbody v-else>
6062
<tr>
61-
<td id="all-done" colspan="4">No API keys found.</td>
63+
<td id="all-done" colspan="5">No API keys found.</td>
6264
</tr>
6365
</tbody>
6466
</table>

cpanfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ requires 'Try::Tiny';
1818
requires 'YAML::XS';
1919
requires 'JSON::Validator';
2020
requires 'Digest::SHA1';
21+
requires 'MCP', '>= 0.06';

docs/UserAPI.md

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Cavil User API
22

3-
## REST API
4-
5-
### Authentication
3+
## Authentication
64

75
All user API endpoints use bearer tokens you can generate with the "API Keys" menu entry after logging into Cavil.
86
These bearer tokens are passed with every request via the `Authorization` header.
@@ -31,6 +29,88 @@ $ curl -H 'Authorization: Bearer generated_api_key_here' https://legaldb.suse.de
3129
...
3230
```
3331

32+
## MCP API
33+
34+
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is a standard that allows Large Language Models
35+
(LLMs) to interact with web services. MCP is supported natively by Cavil. An MCP endpoint is available with API key
36+
authentication under the path `/mcp`.
37+
38+
Many features have access restrictions, and which are available to you will depend on the type of API key being used
39+
(read-only or read-write), and which roles your user account has assigned. Embargoed package updates may not be
40+
processed with AI and are always excluded.
41+
42+
Most MCP clients today support Bearer token authentication, so that is what Cavil relies on as well. More
43+
authentication mechanisms will be added as the technology evolves.
44+
45+
This example configuration in the `mcp.json` format, which is commonly used by MCP clients, shows how to include a
46+
Cavil API key by setting the Authorization HTTP header:
47+
48+
```
49+
{
50+
"mcpServers": {
51+
"cavil": {
52+
"url": "https://legaldb.suse.de/mcp",
53+
"headers": {
54+
"Authorization": "Bearer generated_api_key_here"
55+
}
56+
}
57+
}
58+
}
59+
```
60+
61+
### 3rd Party MCP Clients
62+
63+
While most MCP clients will work with Cavil, we recommend the use of models specifically designed for legal
64+
professionals.
65+
66+
#### gemini-cli
67+
68+
Once you have installed and set up [gemini-cli](https://github.com/google-gemini/gemini-cli), you can use the gemini
69+
mcp command to add Cavil:
70+
71+
```
72+
gemini mcp add cavil https://legaldb.suse.de/mcp -H 'Authorization: Bearer generated_api_key_here' -t http
73+
```
74+
75+
After restarting gemini-cli, it will automatically discover available Cavil tools and make use of them on its own:
76+
77+
```
78+
╭──────────────────────────────────────────────────────────────────────────────╮
79+
│ > You are a legal reviewer for SUSE Linux Enterprise. Take a look at legal │
80+
│ report 1 and make a recommendation if that package update should be │
81+
│ accepted for distribution to customers. │
82+
╰──────────────────────────────────────────────────────────────────────────────╯
83+
84+
╭─────────────────────────────────────────────────────────────────────────────╮
85+
│ ✔ cavil_get_report (cavil MCP Server) {"package_id":1}. │
86+
│ │
87+
│ ... │
88+
╰─────────────────────────────────────────────────────────────────────────────╯
89+
90+
✦ Based on my review of the legal report for perl-Mojolicious version 7.25, I
91+
recommend acceptance of the package for distribution.
92+
93+
Justification:
94+
95+
1. Licenses: The package is primarily licensed under the Artistic License
96+
2.0, with other files covered by compatible and acceptable licenses
97+
such as Apache-2.0, MIT, and CC-BY-SA-4.0. These are all standard and
98+
approved open-source licenses.
99+
2. High-Risk Item: The single file flagged with "Risk 9 (Unknown)",
100+
Mojolicious-7.25/lib/Mojolicious/Guides/Contributing.pod, is a false
101+
positive. The flagged text is a standard part of a contributor's guide,
102+
instructing on code review practices, and is not a license.
103+
3. No Other Issues: The report does not indicate any other legal
104+
encumbrances. The email addresses and URLs are standard for an
105+
open-source project and do not present any concerns.
106+
107+
The package is legally sound and meets the criteria for inclusion in SUSE
108+
Linux Enterprise.
109+
110+
```
111+
112+
## REST API
113+
34114
### Compression
35115

36116
All responses larger than `860` bytes will be automatically `gzip` compressed for user-agents that include an
@@ -76,7 +156,8 @@ Content-Type: application/json
76156

77157
`GET /api/v1/report/<package_id>.<format>`
78158

79-
Get legal report in plain text or JSON format. Note that the exact report format is not static and will change from
159+
Get legal report in plain text or JSON format. Additionally to `txt` and `json`, the extended report format used for
160+
MCP is available with the format identifier `mcp`. Note that the exact report format is not static and will change from
80161
time to time.
81162

82163
**Request:**

lib/Cavil.pm

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
# Copyright (C) 2018-2022 SUSE LLC
2-
#
3-
# This program is free software; you can redistribute it and/or modify
4-
# it under the terms of the GNU General Public License as published by
5-
# the Free Software Foundation; either version 2 of the License, or
6-
# (at your option) any later version.
7-
#
8-
# This program is distributed in the hope that it will be useful,
9-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11-
# GNU General Public License for more details.
12-
#
13-
# You should have received a copy of the GNU General Public License along
14-
# with this program; if not, see <http://www.gnu.org/licenses/>.
15-
1+
# Copyright SUSE LLC
2+
# SPDX-License-Identifier: GPL-2.0-or-later
163
package Cavil;
174
use Mojo::Base 'Mojolicious', -signatures;
185

@@ -103,6 +90,9 @@ sub startup ($self) {
10390

10491
$self->plugin('Cavil::Plugin::Linux');
10592

93+
my $mcp_action = $self->plugin('Cavil::Plugin::MCP');
94+
$self->types->type(mcp => 'text/plain;charset=utf-8');
95+
10696
# Compress dynamically generated content
10797
$self->renderer->compress(1);
10898

@@ -207,8 +197,9 @@ sub startup ($self) {
207197
$public->get('/api/1.0/source')->to('API#source')->name('source_api');
208198

209199
# API with key
200+
$api_key->any('/mcp' => $mcp_action)->name('mcp');
210201
$api_key->get('/api/v1/whoami')->to('API#whoami')->name('whoami_api');
211-
$api_key->get('/api/v1/report/<id:num>' => [format => ['json', 'txt']])->to('Report#report');
202+
$api_key->get('/api/v1/report/<id:num>' => [format => ['json', 'txt', 'mcp']])->to('Report#report');
212203

213204
# API Keys
214205
$logged_in->get('/api_keys')->to('APIKeys#list')->name('list_api_keys');

lib/Cavil/Controller/Pagination.pm

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,26 @@ sub open_reviews ($self) {
8080
$v->optional('offset')->num;
8181
$v->optional('priority')->num;
8282
$v->optional('inProgress');
83+
$v->optional('notEmbargoed');
8384
$v->optional('filter');
8485
return $self->reply->json_validation_error if $v->has_error;
85-
my $limit = $v->param('limit') // 10;
86-
my $offset = $v->param('offset') // 0;
87-
my $priority = $v->param('priority') // 2;
88-
my $in_progress = $v->param('inProgress') // 'false';
89-
my $search = $v->param('filter') // '';
86+
my $limit = $v->param('limit') // 10;
87+
my $offset = $v->param('offset') // 0;
88+
my $priority = $v->param('priority') // 2;
89+
my $in_progress = $v->param('inProgress') // 'false';
90+
my $not_embargoed = $v->param('notEmbargoed') // 'false';
91+
my $search = $v->param('filter') // '';
9092

9193
my $page = $self->packages->paginate_open_reviews(
92-
{limit => $limit, offset => $offset, in_progress => $in_progress, priority => $priority, search => $search});
94+
{
95+
limit => $limit,
96+
offset => $offset,
97+
in_progress => $in_progress,
98+
not_embargoed => $not_embargoed,
99+
priority => $priority,
100+
search => $search
101+
}
102+
);
93103
$self->render(json => $self->_mark_active_packages($page));
94104
}
95105

lib/Cavil/Controller/Report.pm

Lines changed: 5 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
1-
# Copyright (C) 2021 SUSE Linux GmbH
2-
#
3-
# This program is free software; you can redistribute it and/or modify
4-
# it under the terms of the GNU General Public License as published by
5-
# the Free Software Foundation; either version 2 of the License, or
6-
# (at your option) any later version.
7-
#
8-
# This program is distributed in the hope that it will be useful,
9-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11-
# GNU General Public License for more details.
12-
#
13-
# You should have received a copy of the GNU General Public License along
14-
# with this program; if not, see <http://www.gnu.org/licenses/>.
15-
1+
# Copyright SUSE LLC
2+
# SPDX-License-Identifier: GPL-2.0-or-later
163
package Cavil::Controller::Report;
174
use Mojo::Base 'Mojolicious::Controller', -signatures;
185

196
use Mojo::Asset::File;
20-
use Mojo::JSON 'from_json';
217
use Cavil::Util 'lines_context';
228

239
sub report ($self) {
@@ -30,14 +16,13 @@ sub report ($self) {
3016

3117
return $self->render(text => 'not indexed', status => 408) unless $pkg->{indexed};
3218

33-
return $self->render(text => 'no report', status => 408) unless my $report = $self->reports->cached_dig_report($id);
34-
35-
$report = from_json($report);
36-
$self->_sanitize_report($report);
19+
return $self->render(text => 'no report', status => 408)
20+
unless my $report = $self->reports->sanitized_dig_report($id);
3721

3822
$self->respond_to(
3923
json => sub { $self->render(json => {report => $report, package => $pkg}) },
4024
txt => sub { $self->render('reviewer/report', report => $report, package => $pkg) },
25+
mcp => sub { $self->render(text => $self->helpers->mcp_report($id)) },
4126
html => sub {
4227
my $min = $self->app->config('min_files_short_report');
4328
$self->render('reviewer/report', report => $report, package => $pkg, max_number_of_files => $min);
@@ -92,81 +77,4 @@ sub spdx ($self) {
9277
$self->render(template => 'report/waiting', status => 408);
9378
}
9479

95-
sub _sanitize_report ($self, $report) {
96-
97-
# Flags
98-
$report->{flags} = $report->{flags} || [];
99-
100-
# Files
101-
my $files = $report->{files};
102-
my $expanded = $report->{expanded};
103-
my $lines = $report->{lines};
104-
my $snippets = $report->{missed_snippets};
105-
106-
my @missed;
107-
for my $file (keys %$snippets) {
108-
$expanded->{$file} = 1;
109-
my ($max_risk, $match, $license, $spdx) = @{$report->{missed_files}{$file}};
110-
$license = 'Keyword' unless $license;
111-
push(
112-
@missed,
113-
{
114-
id => $file,
115-
name => $files->{$file},
116-
max_risk => $max_risk,
117-
license => $license,
118-
spdx => $spdx,
119-
match => int($match * 1000 + 0.5) / 10.
120-
}
121-
);
122-
}
123-
delete $report->{missed_files};
124-
delete $report->{missed_snippets};
125-
$report->{missed_files} = [sort { $b->{max_risk} cmp $a->{max_risk} || $a->{name} cmp $b->{name} } @missed];
126-
127-
$report->{files} = [];
128-
for my $file (sort { $files->{$a} cmp $files->{$b} } keys %$files) {
129-
my $path = $files->{$file};
130-
push @{$report->{files}}, my $current = {id => $file, path => $path, expand => $expanded->{$file}};
131-
132-
if ($lines->{$file}) {
133-
$current->{lines} = lines_context($lines->{$file});
134-
}
135-
}
136-
137-
# Risks
138-
my $chart = $report->{chart} = {};
139-
my $risks = $report->{risks};
140-
$report->{risks} = {};
141-
my $licenses = $report->{licenses};
142-
for my $risk (reverse sort keys %$risks) {
143-
my $current = $report->{risks}{$risk} = {};
144-
$risk = $risks->{$risk};
145-
146-
for my $lic (sort keys %$risk) {
147-
my $current = $current->{$lic} = {};
148-
my $license = $licenses->{$lic};
149-
my $name = $current->{name} = $license->{name};
150-
$current->{spdx} = $license->{spdx};
151-
152-
my $matches = $risk->{$lic};
153-
my %files = map { $_ => 1 } map {@$_} values %$matches;
154-
$chart->{$name} = keys %files;
155-
156-
$current->{flags} = $license->{flags};
157-
158-
my $list = $current->{files} = [];
159-
for my $file (sort keys %files) {
160-
push @$list, [$file, $files->{$file}];
161-
}
162-
}
163-
}
164-
165-
# Emails and URLs
166-
my $emails = $report->{emails};
167-
$report->{emails} = [map { [$_, $emails->{$_}] } sort { $emails->{$b} <=> $emails->{$a} } keys %$emails];
168-
my $urls = $report->{urls};
169-
$report->{urls} = [map { [$_, $urls->{$_}] } sort { $urls->{$b} <=> $urls->{$a} } keys %$urls];
170-
}
171-
17280
1;

0 commit comments

Comments
 (0)