Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ bin

.vscode-test

# VSCode local settings

.vscode

# yarn v2

.yarn/cache
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ npm install -g @anyproto/anytype-mcp

</details>

## Environment Variables

| Variable | Default | Description |
| ------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPENAPI_MCP_HEADERS` | — | JSON object of headers forwarded to the Anytype API on every request. Required for auth: `{"Authorization":"Bearer <key>", "Anytype-Version":"2025-11-08"}` |
| `ANYTYPE_API_BASE_URL` | `http://127.0.0.1:31009` | Anytype API base URL. Set to `http://localhost:31012` for `anytype-cli`. |
| `MCP_TRANSPORT` | `stdio` | Transport mode. Set to `http` to enable the Streamable HTTP server. |
| `MCP_HOST` | `127.0.0.1` | Host to bind when `MCP_TRANSPORT=http`. |
| `MCP_PORT` | `3666` | Port to listen on when `MCP_TRANSPORT=http`. Must be in range 1024–65535. |
| `MCP_PASSTHROUGH_HEADERS` | `authorization,anytype-version` | Comma-separated list of inbound HTTP header names (lowercase) forwarded from the MCP HTTP client to the Anytype API. Extend with caution — arbitrary headers must not be forwarded. |

### Custom API Base URL

By default, the server connects to `http://127.0.0.1:31009`. For `anytype-cli` (port `31012`) or other custom base URLs, set `ANYTYPE_API_BASE_URL`:
Expand All @@ -102,6 +113,7 @@ By default, the server connects to `http://127.0.0.1:31009`. For `anytype-cli` (
<summary>Example Configuration</summary>

**MCP Client (Claude Desktop, Cursor, etc.):**

```json
{
"mcpServers": {
Expand All @@ -118,6 +130,7 @@ By default, the server connects to `http://127.0.0.1:31009`. For `anytype-cli` (
```

**Claude Code (CLI):**

```bash
claude mcp add anytype \
-e ANYTYPE_API_BASE_URL='http://localhost:31012' \
Expand Down Expand Up @@ -165,6 +178,18 @@ npm run build
npm link
```

### Running in HTTP Transport Mode

Useful for browser-based clients such as [MCP Inspector](https://github.com/modelcontextprotocol/inspector):

```bash
MCP_TRANSPORT=http MCP_HOST=127.0.0.1 MCP_PORT=3666 npm run dev
```

Then connect your MCP client to `http://127.0.0.1:3666/mcp`.

Auth is passed through from the MCP client — set `Authorization: Bearer <YOUR_API_KEY>` and `Anytype-Version: 2025-11-08` in your client's request headers. Alternatively, set `OPENAPI_MCP_HEADERS` as with stdio mode.

## Contribution

Thank you for your desire to develop Anytype together!
Expand All @@ -181,4 +206,4 @@ Thank you for your desire to develop Anytype together!

Made by Any — a Swiss association 🇨🇭

Licensed under [MIT](./LICENSE.md).
Licensed under [MIT](./LICENSE.md).
2 changes: 1 addition & 1 deletion cli/openapi-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import axios from "axios";
import fs from "fs/promises";
import * as fs from "fs/promises";
import { OpenAPIV3 } from "openapi-types";
import { HttpClient, OpenAPIToMCPConverter } from "../src";

Expand Down
31 changes: 22 additions & 9 deletions scripts/__tests__/start-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loadOpenApiSpec, ValidationError } from "../../src/init-server";
import { overrideSpecPath } from "../../src/utils/base-url";

// Reset specPathOverride after each test to avoid inter-test contamination
afterEach(() => overrideSpecPath(undefined));

// Mock fs and axios
vi.mock("node:fs");
Expand Down Expand Up @@ -72,8 +76,9 @@ describe("loadOpenApiSpec", () => {
it("should load a valid OpenAPI spec from local file", async () => {
// Mock fs.readFileSync to return a valid spec
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(validOpenApiSpec));
overrideSpecPath("./test-spec.json");

const result = await loadOpenApiSpec("./test-spec.json");
const result = await loadOpenApiSpec();

expect(result).toEqual(validOpenApiSpec);
expect(fs.readFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), "./test-spec.json"), "utf-8");
Expand All @@ -84,11 +89,12 @@ describe("loadOpenApiSpec", () => {
vi.mocked(fs.readFileSync).mockImplementation(() => {
throw new Error("ENOENT: no such file or directory");
});
overrideSpecPath("./non-existent.json");

// Mock process.exit to prevent actual exit
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

await loadOpenApiSpec("./non-existent.json");
await loadOpenApiSpec();

expect(console.error).toHaveBeenCalledWith("Failed to read OpenAPI specification file:", expect.any(String));
expect(mockExit).toHaveBeenCalledWith(1);
Expand All @@ -97,11 +103,12 @@ describe("loadOpenApiSpec", () => {
it("should handle invalid JSON", async () => {
// Mock fs.readFileSync to return invalid JSON
vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
overrideSpecPath("./invalid.json");

// Mock process.exit
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

await loadOpenApiSpec("./invalid.json");
await loadOpenApiSpec();

expect(console.error).toHaveBeenCalledWith("Failed to parse OpenAPI specification:", expect.any(String));
expect(mockExit).toHaveBeenCalledWith(1);
Expand All @@ -111,8 +118,9 @@ describe("loadOpenApiSpec", () => {
// Mock fs.readFileSync to return a valid YAML spec
const yamlSpec = JSON.stringify(validOpenApiSpec);
vi.mocked(fs.readFileSync).mockReturnValue(yamlSpec);
overrideSpecPath("./test-spec.yaml");

const result = await loadOpenApiSpec("./test-spec.yaml");
const result = await loadOpenApiSpec();

expect(result).toEqual(validOpenApiSpec);
expect(fs.readFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), "./test-spec.yaml"), "utf-8");
Expand All @@ -121,11 +129,12 @@ describe("loadOpenApiSpec", () => {
it("should handle invalid YAML", async () => {
// Mock fs.readFileSync to return invalid YAML
vi.mocked(fs.readFileSync).mockReturnValue("invalid: yaml: :");
overrideSpecPath("./invalid.yaml");

// Mock process.exit
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

await loadOpenApiSpec("./invalid.yaml");
await loadOpenApiSpec();

expect(console.error).toHaveBeenCalledWith("Failed to parse OpenAPI specification:", expect.any(String));
expect(mockExit).toHaveBeenCalledWith(1);
Expand All @@ -136,8 +145,9 @@ describe("loadOpenApiSpec", () => {
it("should load a valid OpenAPI spec from URL", async () => {
// Mock axios.get to return a valid spec
vi.mocked(axios.get).mockResolvedValue({ data: validOpenApiSpec });
overrideSpecPath("http://example.com/api-spec.json");

const result = await loadOpenApiSpec("http://example.com/api-spec.json");
const result = await loadOpenApiSpec();

expect(result).toEqual(validOpenApiSpec);
expect(axios.get).toHaveBeenCalledWith("http://example.com/api-spec.json");
Expand All @@ -146,11 +156,12 @@ describe("loadOpenApiSpec", () => {
it("should handle network errors", async () => {
// Mock axios.get to throw network error
vi.mocked(axios.get).mockRejectedValue(new Error("Network Error"));
overrideSpecPath("http://example.com/api-spec.json");

// Mock process.exit
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

await loadOpenApiSpec("http://example.com/api-spec.json");
await loadOpenApiSpec();

expect(console.error).toHaveBeenCalledWith("Failed to fetch OpenAPI specification from URL:", "Network Error");
expect(mockExit).toHaveBeenCalledWith(1);
Expand All @@ -159,11 +170,12 @@ describe("loadOpenApiSpec", () => {
it("should handle invalid response data", async () => {
// Mock axios.get to return invalid data
vi.mocked(axios.get).mockResolvedValue({ data: "invalid data" });
overrideSpecPath("http://example.com/api-spec.json");

// Mock process.exit
const mockExit = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);

await loadOpenApiSpec("http://example.com/api-spec.json");
await loadOpenApiSpec();

expect(console.error).toHaveBeenCalledWith("Failed to parse OpenAPI specification:", expect.any(String));
expect(mockExit).toHaveBeenCalledWith(1);
Expand All @@ -173,8 +185,9 @@ describe("loadOpenApiSpec", () => {
// Mock axios.get to return a valid YAML spec
const yamlSpec = JSON.stringify(validOpenApiSpec);
vi.mocked(axios.get).mockResolvedValue({ data: yamlSpec });
overrideSpecPath("http://example.com/api-spec.yaml");

const result = await loadOpenApiSpec("http://example.com/api-spec.yaml");
const result = await loadOpenApiSpec();

expect(result).toEqual(validOpenApiSpec);
expect(axios.get).toHaveBeenCalledWith("http://example.com/api-spec.yaml");
Expand Down
11 changes: 6 additions & 5 deletions scripts/start-server.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { ApiKeyGenerator } from "../src/auth/get-key";
import { initProxy, loadOpenApiSpec, ValidationError } from "../src/init-server";
import { determineBaseUrl } from "../src/utils/base-url";
import { determineBaseUrl, overrideSpecPath } from "../src/utils/base-url";

async function generateApiKey(specPath?: string) {
const openApiSpec = await loadOpenApiSpec(specPath);
async function generateApiKey() {
const openApiSpec = await loadOpenApiSpec();
const baseUrl = determineBaseUrl(openApiSpec);
const generator = new ApiKeyGenerator(baseUrl);
await generator.generateApiKey();
}

export async function main(args: string[] = process.argv.slice(2)) {
const [command, specPath] = args;
overrideSpecPath(specPath);
if (!command || command === "run") {
await initProxy(specPath);
await initProxy();
} else if (command === "get-key") {
await generateApiKey(specPath);
await generateApiKey();
} else {
console.error(`Error: Unknown command "${command}"`);
process.exit(1);
Expand Down
Loading
Loading