README
¶
gp
PDDL planning solver exposed as an MCP server and Go library.
gp implements the Graphplan algorithm (Blum & Furst 1997) for STRIPS and ADL planning domains. It ships as:
gp-server-- an MCP server that any AI assistant can callgraphplan-- a Go library for embedding planning in your appspddl-- a PDDL parser (STRIPS + ADL) that feeds into the solver
Background
What is automated planning?
Automated planning finds a sequence of actions that transforms an initial state into a goal state. You describe what you want -- not how to get there.
A planning problem has three parts:
- Domain -- the types of objects and actions available
- Problem -- the specific objects, starting state, and goal
- Plan -- the output: a sequence of grounded actions
PDDL: Planning Domain Definition Language
PDDL is the standard input format for planners. A domain defines predicates (facts about the world) and operators (actions with preconditions and effects):
(define (domain logistics)
(:requirements :strips :typing)
(:types city cargo rocket)
(:predicates (at ?x - cargo ?c - city)
(in ?x - cargo ?r - rocket)
(rocket-at ?r - rocket ?c - city)
(has-fuel ?r - rocket))
(:action load
:parameters (?c - cargo ?r - rocket ?loc - city)
:precondition (and (at ?c ?loc) (rocket-at ?r ?loc))
:effect (and (not (at ?c ?loc)) (in ?c ?r)))
;; ... more actions
)
A problem specifies objects, initial facts, and goals:
(define (problem deliver)
(:domain logistics)
(:objects r1 - rocket a b - cargo
london paris - city)
(:init (at a london) (at b london)
(rocket-at r1 london) (has-fuel r1))
(:goal (and (at a paris) (at b paris))))
The Graphplan algorithm
Graphplan (Blum & Furst 1997) builds a layered planning graph alternating between proposition levels and action levels. At each level it tracks mutual exclusion (mutex) constraints -- pairs of actions or propositions that cannot coexist.
The algorithm:
- Extend the graph one level (add applicable actions, compute new mutexes)
- Check if all goals are present and pairwise non-mutex
- Search backward through the graph for a valid plan
- Repeat until a plan is found or the graph levels off with no solution
Key properties:
- Finds shortest parallel plans (fewest time steps)
- Guaranteed complete -- if a plan exists, it will find it
- Memoization prunes the search space across iterations
Supported PDDL Features
The solver implements a substantial subset of PDDL, covering classical STRIPS planning and many ADL (Action Description Language) extensions.
Requirements
The parser recognizes these :requirements keywords:
| Requirement | Description |
|---|---|
:strips |
Basic STRIPS planning (conjunctive preconditions, positive/negative effects) |
:typing |
Hierarchical type system for objects and parameters |
:equality |
Equality predicates (= ?x ?y) and (not (= ?x ?y)) in preconditions |
:negative-preconditions |
Negated atoms (not (pred ...)) in preconditions |
:disjunctive-preconditions |
Disjunction (or ...) and implication (imply ...) in preconditions |
:existential-preconditions |
Existential quantification (exists ...) in preconditions |
:universal-preconditions |
Universal quantification (forall ...) in preconditions and effects |
:quantified-preconditions |
Shorthand for :existential-preconditions + :universal-preconditions |
:conditional-effects |
Conditional effects (when ...) in action effects |
:derived-predicates |
Derived predicates (axioms) computed via fixed-point evaluation |
:adl |
Shorthand enabling all ADL features above |
Type system
- Typed declarations -- objects, parameters, and constants use
?var - typeorname - typesyntax - Type hierarchy -- subtypes inherit from parent types
(e.g.
truck airplane - vehicle); untyped names default toobject - Type-aware grounding -- action parameters bind only to objects of the declared type or its subtypes
Domain definition
A domain may contain:
:types-- type declarations with optional parent types:constants-- domain-scoped typed objects available to all problems:predicates-- named predicates with typed parameter lists:action-- operator schemas (see below):derived-- derived predicate axioms (see below)
Actions
Action schemas support:
:parameters-- typed parameter list (can be empty):precondition-- a formula describing when the action is applicable:effect-- a formula describing how the world state changes
Formula language
Preconditions and effects share a common formula language. Not all formula types are valid in both contexts:
| Formula | In preconditions | In effects | Description |
|---|---|---|---|
(pred ?x ?y) |
yes | yes | Atomic predicate (positive) |
(not (pred ?x ?y)) |
yes | yes | Negated atom (delete effect or negative precondition) |
(and f1 f2 ...) |
yes | yes | Conjunction |
(or f1 f2 ...) |
yes | -- | Disjunction |
(imply f1 f2) |
yes | -- | Implication (semantically (or (not f1) f2)) |
(forall (?v - T) body) |
yes | yes | Universal quantification |
(exists (?v - T) body) |
yes | -- | Existential quantification |
(when cond eff) |
-- | yes | Conditional effect |
(= ?x ?y) |
yes | -- | Equality constraint (resolved at grounding time) |
(not (= ?x ?y)) |
yes | -- | Inequality constraint (resolved at grounding time) |
Multi-variable quantifiers are supported and are internally nested into single-variable layers:
;; These are equivalent:
(forall (?x - T1 ?y - T2) body)
(forall (?x - T1) (forall (?y - T2) body))
Derived predicates (axioms)
Derived predicates define facts computed from other facts rather than produced by actions. They support:
- Recursive definitions -- a derived predicate can reference itself, enabling transitive-closure patterns (e.g. reachability)
- Chained axioms -- one derived predicate can reference another derived predicate
- Fixed-point evaluation -- axioms are evaluated repeatedly at each proposition level until no new facts are derived
- Full formula body -- the axiom body can use any precondition-valid
formula (
and,or,exists,not, etc.)
;; Recursive transitive closure
(:derived (reachable ?x - location ?y - location)
(or
(connected ?x ?y)
(exists (?z - location)
(and (connected ?x ?z)
(reachable ?z ?y)))))
Conditional effects
The when clause applies an effect only if a condition holds at
effect time. Combined with forall, this enables powerful patterns:
;; Alert every responder present in the zone
(:action sound-alarm
:parameters (?z - zone)
:precondition (exists (?i - incident) (incident-at ?i ?z))
:effect (forall (?r - responder)
(when (at-zone ?r ?z) (alerted ?r))))
Negative preconditions and closed-world assumption
When an action uses (not (pred ...)) in its precondition, the
solver seeds the initial proposition level with negated instances
for all ground atoms of that predicate that are not in the initial
state. This implements the closed-world assumption: any fact not
explicitly stated is assumed false.
Equality
Equality constraints (= ?x ?y) and (not (= ?x ?y)) are
resolved at grounding time by filtering variable bindings. They
do not appear as runtime propositions -- instead, ground actions
are only generated for bindings that satisfy the constraints.
Problem definition
A problem specifies:
:domain-- reference to the domain name:objects-- typed object declarations (combined with domain constants for grounding):init-- ground positive facts defining the initial state:goal-- a formula describing the desired end state (supports the full precondition formula language)
JSON input format
In addition to PDDL text, domains and problems can be specified as JSON. Formulas use a discriminated-union envelope:
{
"type": "forall",
"variable": "?r",
"var_type": "responder",
"body": {
"type": "when",
"condition": {"type": "atom", "predicate": "at-zone", "args": ["?r", "?z"]},
"effect": {"type": "atom", "predicate": "alerted", "args": ["?r"]}
}
}
The type field selects the formula kind: atom, and, or,
imply, forall, exists, or when. Each kind uses the
relevant fields (predicate/args/negated for atoms,
children for and/or, antecedent/consequent for imply,
variable/var_type/body for quantifiers,
condition/effect for when).
Solver properties
- Shortest parallel plans -- the Graphplan algorithm finds plans with the fewest time steps; actions within a step are guaranteed non-interfering and can execute in parallel
- Completeness -- if a plan exists, the solver will find it; if the planning graph levels off with no solution, it reports unsolvable
- Memoization -- failed goal sets are cached at each level, pruning the backward search across iterations
- Configurable depth -- an optional
MaxLevelscap prevents runaway expansion on hard problems
Unsupported PDDL features
The following PDDL features are not supported:
- Numeric fluents (
:fluents,:numeric-fluents) - Temporal planning (
:durative-actions,:timed-initial-literals) - Action costs and preferences
:eithertype unions- Negation on complex formulas (only
(not atom)is allowed, not(not (and ...))) - Object fluents and functions
Examples
Rocket cargo transport
A rocket must transport two pieces of cargo from London to Paris.
domain.pddl
(define (domain rocket)
(:requirements :strips :typing :equality)
(:types rocket place cargo - object)
(:predicates
(at ?x - object ?y - place)
(in ?c - cargo ?r - rocket)
(has-fuel ?r - rocket))
(:action move
:parameters (?r - rocket ?from - place ?to - place)
:precondition (and
(not (= ?from ?to))
(at ?r ?from)
(has-fuel ?r))
:effect (and
(at ?r ?to)
(not (at ?r ?from))
(not (has-fuel ?r))))
(:action load
:parameters (?r - rocket ?p - place ?c - cargo)
:precondition (and (at ?r ?p) (at ?c ?p))
:effect (and
(in ?c ?r)
(not (at ?c ?p))))
(:action unload
:parameters (?r - rocket ?p - place ?c - cargo)
:precondition (and (at ?r ?p) (in ?c ?r))
:effect (and
(at ?c ?p)
(not (in ?c ?r)))))
problem.pddl
(define (problem rocket-2cargo)
(:domain rocket)
(:objects
r1 - rocket
london paris - place
a b - cargo)
(:init
(at r1 london)
(at a london)
(at b london)
(has-fuel r1))
(:goal (and
(at a paris)
(at b paris))))
Plan (3 time steps):
Step 0: load(r1, london, a) load(r1, london, b)
Step 1: move(r1, london, paris)
Step 2: unload(r1, paris, a) unload(r1, paris, b)
Both cargo items load in parallel, the rocket moves, and both unload in parallel -- the shortest possible plan.
Blocks world (Sussman anomaly)
The classic AI planning benchmark. Three blocks must be stacked in order, but the initial configuration forces the planner to undo partial progress.
domain.pddl
(define (domain blocks-world)
(:requirements :strips :typing)
(:types block - object)
(:predicates
(on ?x - block ?y - block)
(on-table ?x - block)
(clear ?x - block)
(holding ?x - block)
(arm-empty))
(:action pick-up
:parameters (?x - block)
:precondition (and
(clear ?x)
(on-table ?x)
(arm-empty))
:effect (and
(holding ?x)
(not (on-table ?x))
(not (clear ?x))
(not (arm-empty))))
(:action put-down
:parameters (?x - block)
:precondition (holding ?x)
:effect (and
(on-table ?x)
(clear ?x)
(arm-empty)
(not (holding ?x))))
(:action stack
:parameters (?x - block ?y - block)
:precondition (and
(holding ?x)
(clear ?y))
:effect (and
(on ?x ?y)
(clear ?x)
(arm-empty)
(not (holding ?x))
(not (clear ?y))))
(:action unstack
:parameters (?x - block ?y - block)
:precondition (and
(on ?x ?y)
(clear ?x)
(arm-empty))
:effect (and
(holding ?x)
(clear ?y)
(not (on ?x ?y))
(not (clear ?x))
(not (arm-empty)))))
problem.pddl (Sussman anomaly)
(define (problem sussman-anomaly)
(:domain blocks-world)
(:objects a b c - block)
(:init
(on c a)
(on-table a)
(on-table b)
(clear c)
(clear b)
(arm-empty))
(:goal (and
(on a b)
(on b c))))
Initial state: C is on A, A and B are on the table. Goal: A on B, B on C (stack: A-B-C from top to bottom).
This is the Sussman anomaly -- you cannot achieve both subgoals independently without undoing one. The planner must unstack C from A before building the target stack.
Emergency response (ADL features)
An emergency domain that combines several ADL features:
- Derived predicate with
exists--can-handleis true when a qualified, available responder exists - Disjunctive precondition --
dispatchrequires the responder to be at the zone or backup to be available - Existential precondition --
sound-alarmfires when any incident exists in the zone - Universal conditional effect --
sound-alarmalerts every responder present in the zone viaforall/when
domain.pddl
(define (domain emergency)
(:requirements
:strips :typing :derived-predicates
:disjunctive-preconditions
:universal-preconditions :conditional-effects)
(:types
responder incident zone - object)
(:predicates
(qualified ?r - responder ?i - incident)
(available ?r - responder)
(at-zone ?r - responder ?z - zone)
(incident-at ?i - incident ?z - zone)
(dispatched ?r - responder ?i - incident)
(alerted ?r - responder)
(can-handle ?i - incident)
(alarm-sounded ?z - zone)
(backup-available ?z - zone))
(:derived (can-handle ?i - incident)
(exists (?r - responder)
(and (qualified ?r ?i)
(available ?r))))
(:action dispatch
:parameters
(?r - responder ?i - incident ?z - zone)
:precondition (and
(qualified ?r ?i)
(available ?r)
(incident-at ?i ?z)
(or (at-zone ?r ?z) (backup-available ?z)))
:effect (and
(dispatched ?r ?i)
(not (available ?r))))
(:action sound-alarm
:parameters (?z - zone)
:precondition
(exists (?i - incident) (incident-at ?i ?z))
:effect (and
(alarm-sounded ?z)
(forall (?r - responder)
(when (at-zone ?r ?z) (alerted ?r)))))
(:action call-backup
:parameters (?z - zone)
:precondition (alarm-sounded ?z)
:effect (backup-available ?z)))
problem.pddl
(define (problem respond-to-fire)
(:domain emergency)
(:objects
medic1 fire1-resp - responder
fire1 - incident
zone-a zone-b - zone)
(:init
(qualified medic1 fire1)
(qualified fire1-resp fire1)
(available medic1)
(available fire1-resp)
(at-zone medic1 zone-a)
(at-zone fire1-resp zone-b)
(incident-at fire1 zone-a))
(:goal (and
(dispatched medic1 fire1)
(alerted medic1)
(alarm-sounded zone-a))))
Plan (1 time step):
Step 0: dispatch(medic1, fire1, zone-a) sound-alarm(zone-a)
The planner dispatches the medic and sounds the alarm in parallel.
sound-alarm uses forall/when to alert every responder in
zone-a (medic1), while dispatch uses the or precondition --
medic1 is already at zone-a so backup is not needed.
Installation
Binary (recommended)
Download the latest release for your platform:
Linux / macOS:
curl -sSL https://raw.githubusercontent.com/byte4ever/gp/master/install.sh | sh
Windows (PowerShell):
irm https://raw.githubusercontent.com/byte4ever/gp/master/install.ps1 | iex
Verify:
gp-server --version
Build from source
go install github.com/byte4ever/gp/cmd/gp-server@latest
Requires Go 1.25+.
MCP Configuration
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"gp": {
"command": "gp-server"
}
}
}
Claude Code
Add to your project's .mcp.json:
{
"mcpServers": {
"gp": {
"command": "gp-server",
"type": "stdio"
}
}
}
Or run directly:
claude mcp add gp gp-server
Other MCP clients
gp-server speaks MCP over stdio. Point any MCP-compatible client
at the gp-server binary with no arguments.
MCP Tools
One-shot solving
| Tool | Parameters | Description |
|---|---|---|
solve_problem |
format (pddl or json), domain, problem |
Parse and solve an ADL planning problem in a single call. |
Session-based solving
For multi-turn workflows where the AI assistant builds up the domain and problem incrementally:
| Tool | Parameters | Description |
|---|---|---|
create_session |
name |
Create a named planning session. |
set_domain |
session, domain |
Set the PDDL domain on a session. |
set_problem |
session, problem |
Set the PDDL problem on a session. |
solve_session |
session |
Solve the problem stored in a session. |
list_sessions |
-- | List all active session names. |
delete_session |
session |
Delete a session by name. |
Response format
All solve tools return a list of time steps. Actions within the same step are guaranteed non-interfering and can be executed in parallel. Steps must be executed sequentially:
{
"solvable": true,
"steps": [
{"time": 0, "actions": ["load(r1, london, a)", "load(r1, london, b)"]},
{"time": 1, "actions": ["move(r1, london, paris)"]},
{"time": 2, "actions": ["unload(r1, paris, a)", "unload(r1, paris, b)"]}
]
}
When no plan exists:
{
"solvable": false,
"error": "no valid plan found"
}
Go Library
Three packages, each usable independently:
go get github.com/byte4ever/gp
graphplan -- solver engine (STRIPS + ADL)
import "github.com/byte4ever/gp/graphplan"
Build domain and problem structs programmatically, then solve:
solver, err := graphplan.NewSolver(nil)
plan, err := solver.Solve(problem)
See graphplan/README.md for full API.
pddl -- parser
import "github.com/byte4ever/gp/pddl"
Parse PDDL strings into graphplan types:
adapter := pddl.NewAdapter()
domain, err := adapter.ParseDomain(domainPDDL)
problem, err := adapter.ParseProblem(problemPDDL, domain)
Supports :strips, :typing, :equality, :disjunctive-preconditions,
:conditional-effects, :quantified-preconditions, and
:derived-predicates requirements.
See pddl/README.md for full API.
mcpserver -- MCP transport
import "github.com/byte4ever/gp/mcpserver"
Embed the MCP server in your own application:
srv, err := mcpserver.NewServer(cfg, solver, adapter, adapter)
srv.Run()
See mcpserver/README.md for full API.
Project Structure
cmd/gp-server/ Entrypoint binary
graphplan/ Solver engine (Graphplan algorithm)
pddl/ PDDL parser and AST-to-graphplan converter
mcpserver/ MCP server wiring and tool handlers
testdata/ PDDL test fixtures
docs/ Design and implementation plans
install.sh Linux/macOS installer
install.ps1 Windows installer
.goreleaser.yaml Cross-platform release config
License
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
gp-server
command
Command gp-server runs the Graphplan MCP server over stdio.
|
Command gp-server runs the Graphplan MCP server over stdio. |
|
Package graphplan implements the Graphplan algorithm for solving STRIPS and ADL planning problems.
|
Package graphplan implements the Graphplan algorithm for solving STRIPS and ADL planning problems. |
|
examples/rocket
command
Command rocket demonstrates solving the rocket transport problem using the graphplan package.
|
Command rocket demonstrates solving the rocket transport problem using the graphplan package. |
|
Package mcpserver exposes the Graphplan planner as an MCP server.
|
Package mcpserver exposes the Graphplan planner as an MCP server. |
|
Package pddl provides a parser for PDDL (Planning Domain Definition Language) files.
|
Package pddl provides a parser for PDDL (Planning Domain Definition Language) files. |
|
examples/parse_rocket
command
Command parse_rocket parses the rocket PDDL files and prints the resulting domain and problem.
|
Command parse_rocket parses the rocket PDDL files and prints the resulting domain and problem. |