Skip to content
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ User-visible changes are recorded here. Alpha releases are for evaluation; stabl
`status: left`, which `permesh check` counts as departed. The starter gains plan
and verify workflows ([access plans](docs/access-plans.md),
[ADR 0034](docs/adr/0034-access-plans.md)).
- Add experimental `permesh mcp`: a local, read-only MCP server (protocol
2026-07-28) whose tools answer user, resource, admin, orphaned and profile
questions, prepare join, move and leave plan files inside the workspace, and
verify plans. Results carry the CLI's report, exit code and completeness. It never
changes access ([AI assistants](docs/mcp.md),
[ADR 0035](docs/adr/0035-read-only-mcp-server.md)).

- Guided `provider add` now supports GitLab, Entra ID and AWS Identity Center.
- `provider list` reports each external provider's ID and whether it has recorded
Expand Down
154 changes: 154 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ reports what is gone, what remains and what can't be checked. Permesh never trea
"not seen" as "removed", and it makes no changes itself. See the
[departure workflow](docs/offboarding.md).

## Ask an AI assistant

`permesh mcp` lets an AI assistant on your machine ask the same questions,
read-only, with the same completeness warnings:

```bash
claude mcp add permesh -- permesh mcp --config "$PWD/permesh.yaml"
```

It can prepare plan files but never changes access. See [AI assistants](docs/mcp.md).

## Connect your systems

In a new directory, separate from the demo:
Expand Down
1 change: 1 addition & 0 deletions crates/permesh-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ rpassword.workspace = true
zeroize = { version = "1", features = ["serde"] }

tempfile.workspace = true
rmcp = { version = "3.7", default-features = false, features = ["server", "transport-io"] }

[dev-dependencies]

Expand Down
2 changes: 2 additions & 0 deletions crates/permesh-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ pub enum Command {
#[arg(value_enum, conflicts_with = "json")]
shell: clap_complete::Shell,
},
/// Serve read-only answers to a local AI assistant over MCP (stdio).
Mcp,
/// Show version and privacy defaults.
Version,
}
Expand Down
11 changes: 11 additions & 0 deletions crates/permesh-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod guided_add;
mod guided_prompt;
mod identity_command;
mod inventory;
mod mcp;
mod offboard;
mod orphaned_output;
mod output;
Expand Down Expand Up @@ -95,6 +96,16 @@ async fn main() -> ExitCode {
}
let schema_version = report::command_schema_version(&cli.command);
let blocking = blocking::BlockingPool::new();
if let args::Command::Mcp = &cli.command {
// stdout belongs to the MCP session; errors only reach stderr.
return match mcp::serve(&cli, &blocking).await {
Ok(()) => ExitCode::SUCCESS,
Err(error) => match output::write_error(&error, false, schema_version) {
Ok(()) => ExitCode::from(error.code),
Err(_) => ExitCode::from(5),
},
};
}
let result = if let args::Command::Provider {
command: args::ProviderCommand::External { command },
} = &cli.command
Expand Down
58 changes: 58 additions & 0 deletions crates/permesh-cli/src/mcp/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
//! Read-only MCP server over stdio (ADR 0035). Tools run existing commands only.
mod paths;
mod server;
mod tools;
use crate::{
args::Cli, blocking::BlockingPool, cancellation::Cancellation, error::AppError, workspace,
};
use rmcp::{ServiceExt, service::ServerInitializeError};
use std::sync::Arc;

/// Resolves on Ctrl+C, or on SIGTERM where it exists (how MCP clients stop servers).
async fn stopped() {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut terminate) = signal(SignalKind::terminate()) {
tokio::select! {
_ = tokio::signal::ctrl_c() => {},
_ = terminate.recv() => {},
}
return;
}
}
let _ = tokio::signal::ctrl_c().await;
}

pub(crate) async fn serve(cli: &Cli, blocking: &BlockingPool) -> Result<(), AppError> {
let workspace = std::fs::canonicalize(workspace::path(cli)?)
.map_err(|_| AppError::input("Cannot locate workspace configuration"))?;
permesh_config::Config::load(&workspace)?;
let lock = Arc::new(tokio::sync::Mutex::new(()));
let shutdown = Cancellation::new();
let handler = server::Server::new(workspace, blocking.clone(), lock.clone(), shutdown.clone());
let session = tokio::select! {
session = handler.serve(rmcp::transport::stdio()) => session,
() = stopped() => return Err(AppError::new(130, "Cancelled")),
};
let running = match session {
Ok(running) => running,
Err(ServerInitializeError::ConnectionClosed(_)) => return Ok(()),
Err(_) => {
return Err(AppError::input(
"The MCP session could not start. permesh mcp supports MCP protocol 2026-07-28 only; update the assistant or its MCP client.",
));
}
};
let ended = tokio::select! {
result = running.waiting() => result
.map(|_| ())
.map_err(|_| AppError::new(5, "The MCP session ended unexpectedly")),
() = stopped() => Err(AppError::new(130, "Cancelled")),
};
// A running call drains like Ctrl+C in the CLI: providers are stopped and reaped first.
shutdown.cancel();
let _drained = lock.lock().await;
ended
}
Loading
Loading