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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: DriftCore CI

on:
push:
branches: [ main ]
pull_request:

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: |
npm install --prefix packages/server
npm install --prefix packages/agent-runner

- name: Lint
run: |
npm run lint --prefix packages/server
npm run lint --prefix packages/agent-runner

- name: Unit tests
run: |
npm run test --prefix packages/server
npm run test --prefix packages/agent-runner

- name: Integration checks
run: |
npm run integration --prefix packages/server
npm run integration --prefix packages/agent-runner
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
**/node_modules/
**/dist/
**/.driftcore/
8 changes: 4 additions & 4 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# DriftCore Examples

This directory will host runnable examples demonstrating the MCP server, agent runner,
and extension integrations.
This directory hosts runnable examples demonstrating the MCP server, agent runner, and Drupal integrations.

- TODO: Add walkthrough for schema resource exploration.
- TODO: Add sandbox execution showcase.
## Available Examples

- [Drupal 11 Sandbox](./drupal-sandbox/README.md) – Docker Compose environment that provisions Drupal, MariaDB, and pre-seeded configuration matching the MCP resources exposed by `@driftcore/server`.
11 changes: 11 additions & 0 deletions examples/drupal-sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM drupal:11-apache

RUN set -eux; \
apt-get update; \
apt-get install -y git unzip mariadb-client; \
rm -rf /var/lib/apt/lists/*;

COPY ./settings.php /var/www/html/sites/default/settings.local.php
COPY ./config /var/www/html/config

RUN chown -R www-data:www-data /var/www/html/config
25 changes: 25 additions & 0 deletions examples/drupal-sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Drupal 11 Sandbox

This Docker Compose environment provisions a minimal Drupal 11 site backed by MariaDB. It mirrors the resources exposed by the DriftCore MCP server and is intended for local experimentation.

## Prerequisites

- Docker
- Docker Compose v2

## Usage

```bash
cd examples/drupal-sandbox
docker compose up --build
```

Once the containers are running, visit [http://localhost:8081](http://localhost:8081) to complete Drupal's installation wizard.

The configuration export directory is mounted at `examples/drupal-sandbox/config/sync` and seeded with the same configuration that is exposed through the MCP server's `config.exported` resource.

## Maintenance

- To rebuild after editing configuration run `docker compose build drupal`.
- Run Drush commands inside the container with `docker compose exec drupal drush status`.
- Use `docker compose down --volumes` to reset the database and configuration.
9 changes: 9 additions & 0 deletions examples/drupal-sandbox/config/sync/system.site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
uuid: 00000000-0000-0000-0000-000000000000
name: 'DriftCore Sandbox'
mail: admin@example.com
slogan: 'Composable automation for Drupal'
page:
403: ''
404: ''
front: /node
langcode: en
31 changes: 31 additions & 0 deletions examples/drupal-sandbox/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: "3.9"

services:
drupal:
build: .
container_name: driftcore-sandbox
ports:
- "8081:80"
environment:
DRUPAL_DB_HOST: database
DRUPAL_DB_NAME: drupal
DRUPAL_DB_USER: drupal
DRUPAL_DB_PASSWORD: drupal
volumes:
- drupal-config:/var/www/html/config
depends_on:
- database

database:
image: mariadb:11
environment:
MARIADB_DATABASE: drupal
MARIADB_USER: drupal
MARIADB_PASSWORD: drupal
MARIADB_ROOT_PASSWORD: root
volumes:
- db-data:/var/lib/mysql

volumes:
drupal-config:
db-data:
12 changes: 12 additions & 0 deletions examples/drupal-sandbox/settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
$databases['default']['default'] = [
'database' => getenv('DRUPAL_DB_NAME') ?: 'drupal',
'username' => getenv('DRUPAL_DB_USER') ?: 'drupal',
'password' => getenv('DRUPAL_DB_PASSWORD') ?: 'drupal',
'host' => getenv('DRUPAL_DB_HOST') ?: 'database',
'driver' => 'mysql',
'prefix' => '',
];

$settings['config_sync_directory'] = '/var/www/html/config/sync';
$settings['trusted_host_patterns'] = ['^localhost$', '^driftcore-sandbox$'];
5 changes: 4 additions & 1 deletion packages/agent-runner/config/default.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"serverEndpoint": "http://localhost:8080",
"transport": "http"
"transport": "http",
"sdkOutputDir": ".driftcore/sdk",
"sandboxRuntime": "node",
"sandboxBootstrapCode": "console.log('Drupal sandbox ready for automation');"
}
3 changes: 3 additions & 0 deletions packages/agent-runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "tsc -p tsconfig.json --pretty --noEmit",
"test": "npm run build && node --test dist/__tests__",
"integration": "npm run build && node dist/integration/smoke.js",
"start": "node dist/index.js"
},
"dependencies": {
Expand Down
34 changes: 34 additions & 0 deletions packages/agent-runner/src/__tests__/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { generateTypeScriptSDK } from "../sdk.js";

const resources = [
{
id: "schema.entityTypes",
name: "Drupal entity type registry",
description: "",
mimeType: "application/json",
data: { entityTypes: [{ machineName: "node", label: "Content", description: "", fields: [] }] },
},
{
id: "config.exported",
name: "Drupal exported configuration",
description: "",
mimeType: "application/json",
data: { settings: { site: { name: "Example" } } },
},
];

describe("generateTypeScriptSDK", () => {
it("writes the SDK file to disk", async () => {
const outDir = await mkdtemp(path.join(tmpdir(), "driftcore-sdk-"));
await generateTypeScriptSDK({ outputDir: outDir, resources, logger: console });
const contents = await readFile(path.join(outDir, "driftcore-sdk.ts"), "utf8");
assert.match(contents, /entityTypes/);
assert.match(contents, /exportedConfiguration/);
});
});
112 changes: 108 additions & 4 deletions packages/agent-runner/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,128 @@
import { generateTypeScriptSDK, type ResourceDescriptor } from "./sdk.js";
import { executeSandbox } from "./sandbox.js";

const fallbackResources: ResourceDescriptor[] = [
{
id: "schema.entityTypes",
name: "Drupal entity type registry",
description: "Local fallback entity registry used during STDIO development.",
mimeType: "application/json",
data: {
entityTypes: [
{
machineName: "node",
label: "Content",
description: "Drupal content entity representing published site content.",
fields: [
{ name: "title", type: "string", description: "Human readable title." },
{ name: "body", type: "text_long", description: "Rich text body field." },
{ name: "uid", type: "entity:user", description: "Reference to the authoring user." },
{ name: "status", type: "boolean", description: "Published flag." },
],
},
{
machineName: "user",
label: "User",
description: "Account entity storing authentication and profile data.",
fields: [
{ name: "name", type: "string", description: "Login username." },
{ name: "mail", type: "email", description: "Primary e-mail address." },
{ name: "roles", type: "string[]", description: "Assigned Drupal roles." },
{ name: "status", type: "boolean", description: "Active state." },
],
},
],
},
},
{
id: "config.exported",
name: "Drupal exported configuration",
description: "Local fallback configuration snapshot used during STDIO development.",
mimeType: "application/json",
data: {
modules: [
{ name: "drupal", type: "core" },
{ name: "toolbar", type: "core" },
{ name: "block", type: "core" },
],
settings: {
site: {
name: "DriftCore Sandbox",
mail: "admin@example.com",
slogan: "Composable automation for Drupal",
},
},
},
},
];

export interface AgentRunnerOptions {
serverEndpoint: string;
transport: "stdio" | "http";
sdkOutputDir: string;
sandboxRuntime: "node" | "deno" | "php";
sandboxBootstrapCode: string;
}

async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url, { headers: { "Accept": "application/json" } });
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}
return (await response.json()) as T;
}

export class AgentRunner {
constructor(private readonly options: AgentRunnerOptions) {}

async start() {
// TODO: Implement real orchestration logic connecting to MCP server.
if (this.options.transport === "stdio") {
console.info("Starting agent runner in STDIO bridge mode");
console.info("STDIO transport selected; falling back to local resource snapshot.");
} else {
console.info(`Connecting to MCP HTTP server at ${this.options.serverEndpoint}`);
}
console.info("Agent runner stub initialized");

const resources = await this.loadResources();
await generateTypeScriptSDK({
outputDir: this.options.sdkOutputDir,
resources,
logger: console,
});

const sandboxResult = await executeSandbox({
code: this.options.sandboxBootstrapCode,
runtime: this.options.sandboxRuntime,
context: { resources },
});

if (sandboxResult.warnings.length > 0) {
sandboxResult.warnings.forEach((warning) => console.warn(warning));
}
sandboxResult.logs.forEach((line) => console.info(`[sandbox] ${line}`));
console.info("Agent runner initialization complete");

return { resources, sandboxResult };
}

private async loadResources(): Promise<ResourceDescriptor[]> {
if (this.options.transport === "http") {
const url = new URL("/resources", this.options.serverEndpoint).toString();
const payload = await fetchJson<{ resources: ResourceDescriptor[] }>(url);
return payload.resources;
}

return fallbackResources;
}
}

export async function createDefaultRunner() {
const runner = new AgentRunner({ serverEndpoint: "http://localhost:8080", transport: "http" });
const runner = new AgentRunner({
serverEndpoint: "http://localhost:8080",
transport: "http",
sdkOutputDir: ".driftcore/sdk",
sandboxRuntime: "node",
sandboxBootstrapCode: "console.log('Drupal sandbox ready for automation');",
});
await runner.start();
return runner;
}
28 changes: 28 additions & 0 deletions packages/agent-runner/src/integration/smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { AgentRunner } from "../index.js";

export async function runAgentIntegration() {
const outputDir = await mkdtemp(path.join(tmpdir(), "driftcore-sdk-"));
const runner = new AgentRunner({
serverEndpoint: "http://localhost:65535", // intentionally unused in HTTP fallback
transport: "stdio",
sdkOutputDir: outputDir,
sandboxRuntime: "node",
sandboxBootstrapCode: "console.log('Sandbox ready')",
});
await runner.start();
}

if (import.meta.url === `file://${process.argv[1]}`) {
runAgentIntegration()
.then(() => {
console.info("Agent runner integration smoke test completed");
process.exit(0);
})
.catch((error) => {
console.error("Agent runner integration smoke test failed", error);
process.exit(1);
});
}
Loading