@ours.network/mcp 0.17.0-nightly.9 → 0.18.0-nightly.1

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.
package/README.md CHANGED
@@ -1,164 +1,74 @@
1
1
  # @ours.network/mcp
2
2
 
3
- Agent-agnostic MCP server for **ours** — a native ADAPT node exposing secure
4
- agent-to-agent messaging tools. It is platform-neutral (no Claude Code / Codex /
5
- Cursor specifics); per-platform plugins depend on this package and run its
6
- `proxy` to reach the daemon.
7
-
8
- The server **is** the node: on startup it boots a single ADAPT packet (a MUFL
9
- messenger), restores prior state from the state dir, connects to the broker, and
10
- exposes the messaging tools — each a thin wrapper over one MUFL user transaction:
11
-
12
- - `generate_invite` — invite to share out-of-band (optionally named; `mode`
13
- `"one_time"` default or `"public"` for a reusable open-posting invite)
14
- - `list_invites` / `revoke_invite` — outstanding invites; revocation is the only
15
- way to close a public invite (idempotent)
16
- - `add_contact` — add a contact from an invite blob (TOFU)
17
- - `list_contacts`
18
- - `remove_contact` — contacts-layer forget + one best-effort authenticated
19
- "remove me" notice to the peer (fire-and-forget; remote removal NOT guaranteed)
20
- - `create_temporary_identity` / `close_temporary_identity` — session-scoped
21
- identity owned by exactly one session lease: on close/session end, contacts get
22
- one best-effort remove-me notice, then ALL local state is deleted (stale ones —
23
- owner process dead — are swept automatically; permanent identities never are)
24
- - `send_message` — end-to-end encrypted; optional `reply_to_wire_id` (+ `reply_to_sentence`) to reply to a specific message
25
- - `get_messages` — return unread messages (bodies, each with its `wire_id` + any `reply_to`) + mark read; delivered exactly once
26
- - `mark_processed` / `defer_messages` — remove handled messages, or re-queue read ones for another session
27
- - `list_incoming_messages` — full inbox with ids + status (read-only)
28
- - `list_incoming_files` — byte-free structured preauthorization metadata, including authenticated sender CID (`from.id`), separate display name, stable IDs, size/date/status, filename and MIME
29
- - `get_files({ wire_ids? })` — atomically retrieve only approved unread wire IDs; omitting `wire_ids` preserves legacy all-unread retrieval. Results include safe local paths, actual size/hash, provenance/status, and structured voice transcription outcomes
30
-
31
- File wake events are content-free but correlation-complete: authenticated `sender_id`,
32
- `file_id`, `wire_id`, display name, filename, MIME, byte count, and received date. A caller
33
- must authorize against `sender_id`, never the untrusted display label. Selected IDs are
34
- unique 64-hex wire IDs (maximum 32); malformed, duplicate, unknown, or stale selections
35
- fail atomically without retrieving or writing any file.
36
-
37
- ## Configuration
38
-
39
- | Env var | Default | Meaning |
40
- |---------|---------|---------|
41
- | `OURS_STATE_DIR` | `~/.ours` | Node identity + serialized state. Distinct per node. |
42
- | `OURS_BROKER_URL` | `wss://broker1.ours.network` | The ADAPT broker to connect through. Set to `ws://localhost:9000` for a local broker. |
43
- | `OURS_SERVICE_NAME` | *(none)* | Boot-service instance name (also `serviceName` in `config.json`). See below. |
44
-
45
- Nightly installers can associate a client application with one local daemon profile in
46
- `~/.ours/installer-profiles.json`. Platform shims pass `--application claude-code`, `codex`, or
47
- `hermes` only to the client commands `proxy` and `watch`; lifecycle commands reject the flag.
48
- Explicit `OURS_CONFIG`, `OURS_PORT`, or `OURS_STATE_DIR` wins. Otherwise the association selects
49
- the config before the historical `~/.ours/config.json` / `3050` fallback.
50
-
51
- An associated client fails closed: the selected config must still resolve the registered port and
52
- state directory; unauthenticated `/info` must report that state directory before the protected API
53
- is probed; and authentication must succeed. Drift, an unreachable daemon, or a 401/403 instructs
54
- the operator to rerun the Nightly installer—there is no silent fallback. Token precedence remains
55
- `OURS_API_TOKEN` → selected config `apiToken` → selected state directory `daemon-token`; tokens are
56
- never read from or written to the registry. Installed associations force client auto-start off.
57
-
58
- ### Running more than one daemon on a host
59
-
60
- `install-service` bakes the resolved port, broker and state directory into a single boot
61
- definition — `ours.service` on Linux, `solutions.adaptframework.ours` on macOS. Two daemons
62
- installed that way would write the **same** definition, so the second silently replaces the
63
- first.
64
-
65
- Give a daemon an instance name and it gets its own definition instead:
3
+ The agent-facing MCP adapter for the shared ours daemon.
4
+
5
+ `ours-mcp` does not contain, start, configure, or install a daemon. Install
6
+ `@ours.network/cli@1.0.1`, configure it with `ours config setup`, and start the
7
+ single shared service with `ours daemon start` (or `ours daemon install-service`).
8
+
9
+ ## MCP configuration
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "ours": {
15
+ "command": "ours-mcp",
16
+ "args": ["proxy"]
17
+ }
18
+ }
19
+ }
20
+ ```
66
21
 
67
- ```sh
68
- OURS_CONFIG=~/.ours-tg/config.json OURS_SERVICE_NAME=tg ours-mcp install-service
69
- # → ours-tg.service / solutions.adaptframework.ours.tg
22
+ `proxy` attaches through `@ours.network/sdk@2.0.1`. It uses the SDK's coherent
23
+ daemon selection (`OURS_CONFIG`, or matching `OURS_PORT` and `OURS_STATE_DIR`)
24
+ and verifies `/state-dir` before credentials are sent. An unavailable daemon is
25
+ reported with install/start guidance; it is never started inside the MCP process.
26
+
27
+ Legacy daemon variables such as `OURS_AUTOSTART`, `OURS_TRANSPORT`, and
28
+ `OURS_UNIT_DIR` are rejected. Named `--application` selections are also rejected;
29
+ use the SDK daemon selection variables instead.
30
+
31
+ ## Application identity list
32
+
33
+ The daemon hosts all identities. ours-mcp keeps only the identities adopted by
34
+ this application and filters global listings through that set. Fresh installs
35
+ start empty. Creating an identity or successfully calling `choose_identity`
36
+ adopts it; closing or removing one deletes it from the application list.
37
+
38
+ The file defaults to `~/.ours-mcp/config.json` and can be overridden for tests
39
+ with `OURS_MCP_CONFIG`. Its versioned shape is:
40
+
41
+ ```json
42
+ {
43
+ "version": 1,
44
+ "daemons": {
45
+ "/absolute/daemon/state-dir": {
46
+ "identities": ["BuildBot"]
47
+ }
48
+ }
49
+ }
70
50
  ```
71
51
 
72
- Named definitions also persist that exact `OURS_CONFIG` path. This is required because visibility,
73
- API-token source, transcription, GC, and other config-only settings must resolve from the same file
74
- after reboot; persisting only port/state/broker would not preserve an auth-isolated profile.
52
+ This list is application bookkeeping, not authorization. `choose_identity`
53
+ remains able to select any daemon identity and adopts it on success. Vanished
54
+ names remain recorded but are not rendered; idempotent close/remove cleans them.
75
55
 
76
- With no name — the default, and every existing deployment — the unit and label are exactly what
77
- they always were. A name must be 1–32 characters of letters, digits, hyphen or underscore,
78
- starting and ending with a letter or digit; anything else is **refused** rather than rewritten,
79
- because falling back to the shared unit is the overwrite this prevents. Removing a named
80
- instance needs the same name: `OURS_SERVICE_NAME=tg ours-mcp uninstall-service`.
56
+ ## Compatibility CLI
81
57
 
82
- An isolated daemon also needs its own `OURS_CONFIG`/`stateDir` and its own port — the API token
83
- lives in the state directory, so two daemons must never share one.
58
+ Former ours-mcp lifecycle entry points remain compatibility aliases that
59
+ delegate argv, stdio, and exit status to `ours daemon`. The `ours` executable
60
+ must be on `PATH`, or its exact path may be provided through `OURS_CLI`. No
61
+ command falls back to an embedded daemon.
84
62
 
85
- ### Voice-message transcription
63
+ `ours-mcp watch [identity]` streams inbound JSON Lines. With no identity argument,
64
+ only live identities in the selected daemon's ours-mcp application list are
65
+ watched.
86
66
 
87
- Voice transcription is off until a provider and key (plus provider-required fields) are
88
- configured. Check readiness without exposing credentials:
67
+ ## Development
89
68
 
90
69
  ```sh
91
- ours-mcp voice-setup
92
- ours-mcp voice-status --json
70
+ npm install
71
+ npm run build --workspace @ours.network/mcp
72
+ npm run typecheck --workspace @ours.network/mcp
73
+ npm test --workspace @ours.network/mcp
93
74
  ```
94
-
95
- `voice-setup` presents a keyboard-driven single-choice provider selector and reads the
96
- provider key with hidden input. It writes config atomically with mode `0600`, preserves
97
- unrelated fields, and rolls back if the managed daemon cannot restart and report ready.
98
- It never accepts a key in command arguments. For environment-only service configuration
99
- use `OURS_STT_PROVIDER`, `OURS_STT_API_KEY`, `OURS_STT_MODEL`, `OURS_STT_BASE_URL`, and
100
- `OURS_STT_LANGUAGE`; environment values override file fields.
101
-
102
- The supported providers are `openai-compatible` (explicit base URL + model),
103
- `elevenlabs` (model), `deepgram`, and `custom` (`stt.custom.url`). Incoming voice remains
104
- a file and is transcribed only when its real `audio/*` MIME also carries
105
- `x-ours-kind=voice-message` (or its legacy filename starts `voice-message-`). The original
106
- bytes are saved whether transcription succeeds, is unconfigured, exceeds the size cap, or
107
- the provider fails. `get_files` preserves the human transcript/fallback line and also returns
108
- a secret-free structured outcome (`configured`, `attempted`, `status`, `provider`, `text`,
109
- `error_category`, `audio_path`, and `file_wire_id`). Provider error text is scrubbed if it
110
- echoes the configured key and raw provider diagnostics are not copied into structured output.
111
-
112
- Telegram fallback preserves its original OGG/Opus bytes and `.ogg` filename and advertises
113
- `audio/ogg; x-ours-kind=voice-message`; the connector's v2 message envelope correlates the
114
- separate file using `attachment.wire_id`.
115
-
116
- ## Daemon lifecycle
117
-
118
- This package is the **single owner of the daemon lifecycle**. `ours-mcp start`
119
- runs one long-lived HTTP daemon per host (default port 3050) that hosts every
120
- identity's packet, the broker socket, and file locks — a shared singleton that
121
- cannot be run per session. Each session instead runs a thin `ours-mcp proxy`
122
- (stdio ⇄ the daemon's HTTP endpoint), which auto-starts the daemon if it is down.
123
- Platform plugins ship only the proxy invocation; they never own or restart the
124
- daemon.
125
-
126
- `ours-mcp start` and `ours-mcp restart` do not treat an open socket as readiness.
127
- They wait for an authenticated response from the daemon's normal control
128
- surface (`/identities`) after the protocol runtime, contact-book registrar,
129
- persisted identities, and boot reconciliation are complete. In owner mode the
130
- CLI dynamically discovers the mode-`0600` token minted by the daemon before it
131
- declares readiness; shared and open modes preserve their configured auth
132
- semantics. During that wait the daemon publishes a mode-`0600`
133
- `startup-progress.json` in its state directory. The structured record contains
134
- only a phase, timestamps, process/boot identifiers, and identity counts — never
135
- identity names, container IDs, keys, packet contents, or state paths.
136
-
137
- Interactive terminals update one progress line; redirected/noninteractive runs
138
- emit concise stable lines such as `startup: Restoring identities 3/12`. A
139
- heartbeat distinguishes active work from a frozen process: 30 seconds without
140
- an update is a failure, and an absolute three-minute bound prevents an
141
- event-loop-active stall from waiting forever. Immediate daemon failure remains
142
- nonzero. The same daemon bootstrap/reporting path is used by foreground
143
- `serve`, Linux systemd, macOS launchd, and either native or WASM-backed ADAPT
144
- runtimes; service managers keep their existing lifecycle behavior.
145
-
146
- On connect, the proxy runs a compatibility handshake against the daemon's
147
- `/state-dir` report (`{ version, compat }`). `compat` is the wire-contract
148
- version (`src/protocol.ts`) — distinct from the package version, bumped only on
149
- breaking proxy↔daemon changes. Matching `compat` proceeds; a differing package
150
- version warns (stderr); an incompatible `compat` refuses with guidance to run
151
- `ours-mcp stop`. The proxy never kills the shared daemon itself, since it may
152
- be hosting other sessions' identities.
153
-
154
- ## Build
155
-
156
- ```sh
157
- npm run build # esbuild → minified dist/{index,cli}.js + dist/mufl_code/*.muflo
158
- npm run build:dev # readable build (unminified, intact stack traces)
159
- npm run typecheck
160
- npm run dev # run the daemon under tsx
161
- ```
162
-
163
- See the [repo README](https://github.com/adapt-toolkit/ours-mcp#readme) for install
164
- and quickstart.
@@ -0,0 +1,2 @@
1
+ import{randomUUID as g}from"node:crypto";import{chmod as l,mkdir as u,readFile as w,rename as h,unlink as I,writeFile as v}from"node:fs/promises";import{homedir as E}from"node:os";import{dirname as A,resolve as s}from"node:path";var m=1,N="OURS_MCP_CONFIG",P=()=>({version:1,daemons:{}});function S(e=process.env){let t=(e[N]??"").trim();return s(t||s(E(),".ours-mcp","config.json"))}function C(e,t){let i;try{i=JSON.parse(e)}catch(o){throw new Error(`Invalid ours-mcp application identity config at ${t}: ${String(o)}`)}if(!i||typeof i!="object"||Array.isArray(i))throw new Error(`Invalid ours-mcp application identity config at ${t}: expected an object.`);let n=i;if(n.version!==m)throw new Error(`Unsupported ours-mcp application identity config version at ${t}: expected ${m}, found ${String(n.version)}.`);if(!n.daemons||typeof n.daemons!="object"||Array.isArray(n.daemons))throw new Error(`Invalid ours-mcp application identity config at ${t}: "daemons" must be an object.`);let r={};for(let[o,a]of Object.entries(n.daemons)){if(!a||typeof a!="object"||Array.isArray(a))throw new Error(`Invalid ours-mcp application identity config at ${t}: daemon ${JSON.stringify(o)} must be an object.`);let c=a;if(!Array.isArray(c.identities)||c.identities.some(p=>typeof p!="string"||p.length===0))throw new Error(`Invalid ours-mcp application identity config at ${t}: daemon ${JSON.stringify(o)} has an invalid identity list.`);r[s(o)]={identities:[...new Set(c.identities)].sort()}}return{version:1,daemons:r}}async function d(e){try{return C(await w(e,"utf8"),e)}catch(t){if(t.code==="ENOENT")return P();throw t}}async function f(e,t){let i=A(e);await u(i,{recursive:!0,mode:448});let n=`${e}.${process.pid}.${g()}.tmp`;try{await v(n,`${JSON.stringify(t,null,2)}
2
+ `,{mode:384,flag:"wx"}),await h(n,e),await l(e,384)}catch(r){try{await I(n)}catch{}throw r}}var y=class{stateDir;path;constructor(t,i={}){this.stateDir=s(t),this.path=s(i.path??S(i.env))}async list(){return[...(await d(this.path)).daemons[this.stateDir]?.identities??[]]}async has(t){return(await this.list()).includes(t)}async add(t){if(!t)throw new Error("Cannot add an empty identity name to ours-mcp.");let i=await d(this.path),n=i.daemons[this.stateDir]?.identities??[];n.includes(t)||(i.daemons[this.stateDir]={identities:[...n,t].sort()},await f(this.path,i))}async remove(t){let i=await d(this.path),n=i.daemons[this.stateDir];if(!n?.identities.includes(t))return;let r=n.identities.filter(o=>o!==t);r.length===0?delete i.daemons[this.stateDir]:i.daemons[this.stateDir]={identities:r},await f(this.path,i)}};async function b(e,t){let i=new Set(await e.list());return t.filter(n=>i.has(n.name))}export{N as APPLICATION_IDENTITIES_ENV,m as APPLICATION_IDENTITIES_VERSION,y as ApplicationIdentityStore,S as applicationIdentityConfigPath,b as filterApplicationIdentities};