obsidian-modified-mcp-server
Provides tools for managing an Obsidian vault, including reading, writing, searching, appending, and deleting files, as well as graph analytics, semantic search, and periodic notes access via the Obsidian Local REST API.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@obsidian-modified-mcp-server find notes about project management using semantic search"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Obsidian Modified MCP Server
This is a personal fork of @connorbritain/obsidian-mcp-server by Connor Britain. Its purpose is to mitigate wrapper-side limitations of the Local-REST-API-based MCP server. Concrete changes so far: re-enabled the patch_content tool under a structural-only path validator; added two surgical-read tools (get_heading_contents, get_frontmatter_field); wired the seven graph tools through the dispatcher (they previously advertised schemas but returned Unknown tool at runtime); made delete_file recursive on directory paths with timeout-coherent responses; switched the post-timeout verification query to a direct-path probe (so an upstream auto-prune of the parent directory no longer surfaces a successful delete as outcome undetermined); and exposed the upstream's authoritative tag index via a new list_tags tool that includes both inline and frontmatter tags and excludes tag-shaped strings inside fenced code blocks — more accurate than text or frontmatter search for tag enumeration. Subsequent specs will add similar wrapper-level mitigations as the fork evolves.
Status: Personal fork. External support not guaranteed; use at your own discretion.
TypeScript MCP server for Obsidian with core vault operations, graph analytics, and semantic search.
Features
Core Tools: Read, write, search, append, delete files in your Obsidian vault
Periodic Notes: Access daily, weekly, monthly notes and recent changes
Advanced Search: JsonLogic queries for complex filtering
Graph Tools: Orphan detection, centrality analysis, cluster detection, path finding
Semantic Search: Smart Connections integration for concept-based search
Related MCP server: mcp-markdown-vault
Differences from upstream
Change | Description | Rationale |
| Heading/block/frontmatter PATCH tool is enabled in this fork under a structural-only path validator. | Wraps the same upstream endpoint Connor's fork disabled. The empirically-observed |
| Two new MCP read tools that fetch part of a vault note instead of the whole file. | Avoids round-tripping the entire file through the MCP transport just to read one section or one field; surfaces the upstream Local REST API's surgical-read endpoints ( |
Graph tools wired through dispatcher | The seven graph tools ( | Previously the seven tools advertised JSON schemas at the catalog layer but returned |
| Directory paths are deleted recursively in a single tool call — the wrapper walks contents in upstream listing order, deletes each file and subdirectory, then deletes the outer directory and returns | Upstream |
| New MCP tool that exposes the upstream Local REST API plugin's | The existing text and frontmatter search tools systematically over-count (they hit code-block mentions) and under-count (they miss inline tags when only frontmatter is searched, or vice versa). Sourcing tag enumeration directly from Obsidian's own index is the only way to give an LLM caller a trustworthy starting point for tag-driven navigation, audit, or cleanup. Live contract in |
In flight (design landed, implementation pending)
The following specs have their full design + spike-blocked scaffold landed in this repo, but their tools are intentionally not yet exposed via tools/list — they're awaiting a build-time prerequisite. They appear here so readers can find the design docs without being misled into thinking the tools are usable now.
Spec | Status | Why pending |
| Design + scaffold landed in v0.5.1. The 2026-05-02 T002 feasibility spike confirmed the original Option-A design (dispatching Obsidian's "Rename file" command via | Build-time dependency on a future |
Heading-path discipline (patch_content, get_heading_contents)
To avoid the disambiguation issue tracked in upstream issue
coddingtonbear/obsidian-local-rest-api#146,
this fork applies a structural validator at the MCP wrapper boundary
before any HTTP call is made. The same rule applies to
patch_content's heading targets and to get_heading_contents's
heading argument — there is exactly one definition of the predicate
across the codebase.
Heading targets MUST be path-shaped. At least two non-empty
::-separated segments, full path from the document's H1 downward. Use"About This Vault::Frontmatter Conventions", not"Frontmatter Conventions". Bare names are rejected with an actionable error message that names the rule, quotes the offending value, and shows a corrected example.Headings whose literal text contains
::are unreachable through these tools — the validator treats every::as a path separator and there is no escape syntax. Fall back toget_file_contents+put_content(write side) orget_file_contents+ client-side slicing (read side).Top-level-only headings (i.e., files with no
::-separable nesting) are also unreachable through these tools. Same fallback.patch_content'sblockandfrontmattertarget types pass through to the upstream unchanged.get_heading_contentsreturns just the raw markdown body under the targeted heading — frontmatter, tags, and file metadata are not included. For frontmatter useget_frontmatter_field(single field) orget_file_contents(whole note).Upstream errors propagate verbatim with status code and message preserved (no silent fallbacks). For
get_frontmatter_fieldin particular, a present-but-nullfield value ({"value":null}) is distinct from a missing field (upstream 4xx surfaced asisError).
These limitations are also stated in each tool's MCP description
field, so they are visible to any caller that lists the available tools.
Prerequisites
Node.js 18+
Obsidian with Local REST API plugin installed and enabled
(Optional) Dataview plugin for
get_recent_changes(Optional) Periodic Notes plugin for periodic note tools
(Optional) Smart Connections plugin for semantic search
Installation
From npm
npm install -g @marwansaab/obsidian-modified-mcp-serverFrom source
git clone https://github.com/marwansaab/obsidian-modified-mcp-server.git
cd obsidian-modified-mcp-server
npm install
npm run buildConfiguration
Set the following environment variables:
Variable | Required | Default | Description |
| Yes* | - | API key from Local REST API plugin settings (used when multi-vault JSON is not supplied) |
| No |
| Obsidian REST API host |
| No |
| Obsidian REST API port |
| No |
|
|
| No | - | Path to vault (required for graph tools) |
| No | - | Port for Smart Connections API |
| No |
| Graph cache TTL in seconds |
| No | - | JSON string describing one or more vaults. Overrides the single |
| No | - | Path to a JSON file describing one or more vaults (same shape as |
| No | first defined | Name/ID of the vault to use when a tool call omits |
Multi-vault note: If neither
OBSIDIAN_VAULTS_JSONnorOBSIDIAN_VAULTS_FILEis provided, the legacy single-vault env vars (OBSIDIAN_API_KEY,OBSIDIAN_HOST, etc.) are used to create adefaultvault entry automatically.
Example OBSIDIAN_VAULTS_JSON
[
{
"id": "work",
"apiKey": "work-api-key",
"host": "127.0.0.1",
"port": 27124,
"protocol": "https",
"vaultPath": "C:/Users/you/Obsidian/work",
"smartConnectionsPort": 29327
},
{
"id": "personal",
"apiKey": "personal-api-key",
"vaultPath": "C:/Users/you/Obsidian/personal"
}
]Each tool in the MCP server accepts an optional vaultId argument. When omitted, the server uses OBSIDIAN_DEFAULT_VAULT (or the first defined vault). This allows a single MCP session to read/write multiple vaults just by specifying which vault to target in the tool call.
Multi-Vault Port Configuration
Important: When running multiple Obsidian vaults simultaneously, each vault's Local REST API plugin must listen on a unique port. By default, all vaults use port
27124, which causes conflicts—only one vault can bind to a port at a time, and requests to other vaults will fail with authorization errors.
Step 1: Assign Unique Ports in Obsidian
For each vault, open Settings → Community Plugins → Local REST API and scroll to Advanced Settings:
Set Encrypted (HTTPS) Server Port to a unique value (e.g.,
27124,27125,27126,27127)Toggle the plugin off and back on (or restart Obsidian) to apply the change
Copy the API Key shown in the plugin settings
Step 2: Update Your Vaults JSON
In your obsidian-vaults.json file (or OBSIDIAN_VAULTS_JSON env var), specify the port for each vault to match what you configured in the plugin:
[
{
"id": "vault_one",
"apiKey": "your-api-key-for-vault-one",
"port": 27124,
"vaultPath": "C:/Users/you/Obsidian/vault_one"
},
{
"id": "vault_two",
"apiKey": "your-api-key-for-vault-two",
"port": 27125,
"vaultPath": "C:/Users/you/Obsidian/vault_two"
},
{
"id": "vault_three",
"apiKey": "your-api-key-for-vault-three",
"port": 27126,
"vaultPath": "C:/Users/you/Obsidian/vault_three"
}
]Step 3: Restart Your MCP Client
After updating the JSON file, restart your MCP client (Windsurf, Claude Desktop, etc.) so it reloads the configuration with the new ports.
Verifying Connectivity
You can test each vault's API directly with curl:
# Replace PORT and API_KEY for each vault
curl -k -H "Authorization: Bearer YOUR_API_KEY" https://127.0.0.1:PORT/vault/A successful response returns a JSON object with the vault's file listing. If you receive 40101 Authorization required, the API key doesn't match. If you receive 40400 Not Found, the plugin isn't fully initialized on that port—try toggling it off/on or restarting the vault.
MCP Client Configuration
Using npx (Recommended)
Use npx for the simplest setup:
{
"mcpServers": {
"obsidian": {
"command": "npx",
"args": ["-y", "@marwansaab/obsidian-modified-mcp-server"],
"env": {
"OBSIDIAN_API_KEY": "your-api-key-here",
"OBSIDIAN_VAULT_PATH": "/path/to/your/vault",
"OBSIDIAN_VAULTS_FILE": "C:/path/to/vaults.json",
"OBSIDIAN_DEFAULT_VAULT": "work"
}
}
}
}Using Local Build (Development)
If running from source:
{
"mcpServers": {
"obsidian": {
"command": "node",
"args": ["/absolute/path/to/obsidian-modified-mcp-server/dist/index.js"],
"env": {
"OBSIDIAN_API_KEY": "your-api-key-here",
"OBSIDIAN_VAULT_PATH": "/path/to/your/vault",
"OBSIDIAN_VAULTS_JSON": "[{\"id\":\"work\",\"apiKey\":\"...\",\"vaultPath\":\"/work\"}]"
}
}
}
}Config File Locations
Client | Config Path |
Claude Desktop (Windows) |
|
Claude Desktop (Mac/Linux) |
|
Windsurf |
|
Cursor |
|
Available Tools
All tools accept an optional vaultId argument. If omitted, the server uses the default vault from your configuration. This lets you read/write multiple Obsidian vaults within the same MCP session.
Path separators: every tool that takes a filepath (or source / target) argument accepts forward-slash, backslash, or mixed separators uniformly across platforms. Forward-slash is the canonical form, but Windows-style backslash paths work without modification. See specs/006-normalise-graph-paths/.
Vault Management
Tool | Description |
| List all configured vaults with their IDs, capabilities, and connection info |
Core File Operations
Tool | Description |
| List all files/directories in vault root |
| List files in a specific directory |
| Read a single file |
| Read multiple files concatenated with headers |
| Delete a file or directory. Directory paths are deleted recursively — the wrapper removes every contained file and subdirectory before deleting the directory itself, in a single tool call. On a transport timeout the wrapper verifies post-condition via a single direct-path query against the deleted target (404 → success, 200 → |
Surgical Read Operations
Tool | Description |
| Read just the raw markdown body under a fully-pathed heading ( |
| Read one frontmatter field's value with its original type preserved (string, number, boolean, array, object, or |
Tag Operations
Tool | Description |
| List every tag in the vault with its usage count, sourced from the upstream's authoritative |
Write Operations
Tool | Description |
| Append to file (creates if missing) |
| Overwrite file content |
| Insert content relative to a heading, block, or frontmatter target. Heading targets must use the full |
| Vault-wide string-replacement across every |
Search
Tool | Description |
| Keyword search across vault |
| JsonLogic query search (glob, regexp support) |
| Regex pattern extraction with context (requires vault path) |
Periodic Notes & Recent Changes
Tool | Description |
| Get current daily/weekly/monthly/quarterly/yearly note |
| Get recent periodic notes with optional content |
| Get recently modified files (requires Dataview) |
Obsidian Integration
Tool | Description |
| Get the currently active file in Obsidian |
| Open a file in Obsidian |
| List all available Obsidian commands |
| Execute one or more Obsidian commands |
Graph Tools (requires OBSIDIAN_VAULT_PATH)
Each graph tool requires OBSIDIAN_VAULT_PATH to be set for the targeted vault. The two per-note tools (get_note_connections, find_path_between_notes) return note not found: <path> when the target note is not present in the vault — distinct from "found but no connections" (success with empty arrays) and "no path between endpoints" (success with path: null). Aggregation tools wrap their primary result in an envelope with skipped and skippedPaths (up to 50 entries) describing files skipped during the build because of read or parse errors. Full I/O contracts live under specs/004-fix-graph-tools/contracts/.
Tool | Description | Contract |
| Overview stats (notes, links, orphans, clusters) | |
| Folder tree structure of vault | |
| Notes with no incoming/outgoing links | |
| Incoming/outgoing links + tags for a note. Returns | |
| Shortest link path between two notes. Returns | |
| Top notes by link count or PageRank | |
| Community detection via graph analysis |
Semantic Tools (requires Smart Connections plugin)
Tool | Description |
| Conceptual search via Smart Connections |
| Find semantically similar notes |
Development
# Watch mode
npm run dev
# Lint
npm run lint
# Type check
npm run typecheck
# Build
npm run build
# Run the test suite (vitest + nock-mocked HTTP, V8 coverage gate)
npm test
# Run tests in watch mode
npm run test:watchSee TESTING.md for the coverage gate's floor, the ratchet procedure, and the AS-IS-vs.-fork-authored test directory convention.
Project Constitution & Spec-Driven Workflow
This repo uses Spec Kit for non-trivial
features. The project constitution (principles every contribution must
honor — modular code, public-tool tests, zod boundary validation, explicit
upstream error propagation) lives in
.specify/memory/constitution.md.
Per-feature specs, plans, contracts, and task lists live under
specs/. Pull requests should confirm that constitution
Principles I–IV were considered.
Attributions
find_and_replace (feature 013)
find_and_replace is composed of three layers, two of which carry attribution to upstream Obsidian-MCP projects:
LAYER 1 — Per-note replacement primitive: algorithm credited to
cyanheads/obsidian-mcp-server'sobsidian_replace_in_notetool (Apache-2.0). The single-pass left-to-right scan over a single note follows that project's pattern; JavaScript's nativeString.prototype.replaceAllandString.prototype.replace(/.../g, ...)provide the actual replacement work. Source-header attribution lives insrc/tools/find-and-replace/pattern-builder.tsandsrc/tools/find-and-replace/replacer.ts.LAYER 2 — Vault-wide composition + dry-run: pattern credited to
blacksmithers/vaultforge'sgrep-subtool (MIT). The dry-run-with-preview concept and the vault-walk strategy are borrowed; the exact preview shape (structured per-match objects with line/column/before/after fields, see FR-015) is this project's own design. Source-header attribution lives insrc/tools/find-and-replace/region-detector.tsandsrc/tools/find-and-replace/preview-formatter.ts.LAYER 3 — Multi-vault dispatch wrapper: original contribution of this project. Wraps LAYER 1 + LAYER 2 with the existing
getRestService(vaultId)plumbing (inherited from Connor Britain's upstream and hardened across multiple configured vaults) so the entire find-and-replace surface routes per-vault by default. None of cyanheads, vaultforge, or MCPVault provides this. Source attribution lives insrc/tools/find-and-replace/walker.ts,src/tools/find-and-replace/response-builder.ts, and thefindAndReplacemethod onsrc/services/obsidian-rest.ts.
The corresponding feature spec, plan, and contracts live in specs/013-find-and-replace/.
License
MIT — see LICENSE. Copyright is held by Connor England (upstream author); this fork's modifications are released under the same MIT terms.
Available Tools
32 toolsappend_contentA
Append content to a file. Creates the file if it does not exist.
Inputs: content (required), filepath (required), vaultId.
batch_get_file_contentsA
Return the contents of multiple files concatenated with headers.
Inputs: filepaths (required), vaultId.
complex_searchB
Search using JsonLogic query. Supports glob and regexp operators for pattern matching.
Inputs: query (required), vaultId.
delete_fileA
Delete a file or directory from the vault. When the path refers to a directory, the deletion is recursive: every contained file and subdirectory is removed before the directory itself is deleted, in a single tool call. The caller does not need to empty the directory beforehand. On a transport-layer timeout the wrapper performs a single direct-path verification query before reporting outcome, so the response always reflects the actual post-condition on the vault.
Inputs: filepath (required), vaultId.
detect_note_clustersA
Detect communities/clusters of related notes using graph analysis. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault.
Inputs: vaultId, minClusterSize.
execute_commandA
Execute one or more Obsidian commands in order. For commands that operate on notes, open a note first.
Inputs: commands (required), vaultId.
find_and_replaceA
Find and replace text vault-wide across every .md file in the targeted vault. DESTRUCTIVE: this tool rewrites notes in-place. Run with dryRun: true first to preview matches; commit with dryRun: false. The vault SHOULD be in a clean git working tree (or otherwise backed up) before mutations — dry-run is the safety net. Concurrency posture is last-write-wins: if Obsidian (or a sync plugin) writes a note in the gap between the tool's read and write, the tool overwrites that external edit without warning. Close Obsidian or pause sync plugins before running mutations on important content. pathPrefix matching is case-sensitive on all platforms (including Windows) and is a directory-segment prefix (no glob expansion). Files in dot-prefixed directories (e.g., .obsidian/, .trash/) are excluded; the per-file size cap is 5 MB on both input and output.
Inputs: search (required), replacement (required), regex, dryRun, +8 more.
find_orphan_notesA
Find notes with no incoming or outgoing links. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault.
Inputs: vaultId, includeBacklinks.
find_path_between_notesA
Find the shortest link path between two notes. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault. Returns a precondition-style error 'note not found: ' when the target note is not present in the vault — distinct from 'found but no connections'/'no path between endpoints'.
Inputs: source (required), target (required), vaultId, maxDepth.
find_similar_notesB
Find notes semantically similar to a given note.
Inputs: filepath (required), limit, vaultId, threshold.
get_active_fileA
Get the currently active file in Obsidian.
Inputs: vaultId.
get_file_contentsA
Returns the content of a single file in your vault.
Inputs: filepath (required), vaultId.
get_frontmatter_fieldA
Returns the named frontmatter field's value with its original type preserved — string, number, boolean, array, object, or null. If the field or the note does not exist, the upstream's 4xx error is propagated unchanged. To read all frontmatter fields at once, use get_file_contents.
Inputs: field (required), filepath (required), vaultId.
get_heading_contentsA
Returns the raw markdown body content under the targeted heading. Frontmatter, tags, and file metadata are not included — use get_file_contents for the whole note or get_frontmatter_field for individual frontmatter values. Heading targets MUST use the full path of the heading: at least two non-empty segments separated by "::" (i.e., the H1::H2[::H3...] form). Top-level headings (no parent) are unreachable through this tool — use get_file_contents and slice the note client-side. Headings whose literal text contains "::" are also unreachable through this tool (the validator treats every "::" as a path separator and there is no escape syntax) — use get_file_contents in that case as well.
Inputs: heading (required), filepath (required), vaultId.
get_most_connected_notesA
Get the most connected notes by link count or PageRank. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault.
Inputs: limit, metric, vaultId.
get_note_connectionsA
Get all connections for a note: outgoing links, backlinks, tags. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault. Returns a precondition-style error 'note not found: ' when the target note is not present in the vault — distinct from 'found but no connections'/'no path between endpoints'.
Inputs: filepath (required), depth, vaultId.
get_periodic_noteC
Get the current periodic note for a specified period.
Inputs: period (required), type, vaultId.
get_recent_changesB
Get recently modified files in the vault.
Inputs: days, limit, vaultId.
get_recent_periodic_notesB
Get most recent periodic notes for a specified period type.
Inputs: period (required), limit, vaultId, include_content.
get_vault_statsA
Get overview statistics about the vault: total notes, links, orphans, tags, clusters. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault.
Inputs: vaultId.
get_vault_structureA
Get the folder tree structure of the vault. Requires OBSIDIAN_VAULT_PATH to be set for the targeted vault.
Inputs: vaultId, maxDepth, includeFiles.
list_commandsA
List all available commands in Obsidian. For commands that operate on notes, open a note first.
Inputs: vaultId.
list_files_in_dirA
Lists all files and directories in a specific directory within your Obsidian vault.
Inputs: dirpath (required), vaultId.
list_files_in_vaultA
Lists all files and directories in the root of your Obsidian vault.
Inputs: vaultId.
list_tagsA
List every tag present in the vault, together with its usage count. The result is sourced from Obsidian's own tag index via the Local REST API plugin's GET /tags/ endpoint, so it includes both inline (#tag) and YAML frontmatter tags and excludes tag-shaped strings that appear inside fenced code blocks — making it more accurate than text or frontmatter search for tag enumeration. Hierarchical tags (e.g., work/tasks) contribute counts to every parent prefix (e.g., work), matching how Obsidian's own tag sidebar displays them.
Inputs: vaultId.
list_vaultsA
List all configured Obsidian vaults with their IDs and capabilities.
Inputs: no named parameters.
open_fileC
Open a file in Obsidian.
Inputs: filepath (required), vaultId.
patch_contentA
Insert content relative to a heading, block, or frontmatter in a file. For heading targets, the value MUST be the full path of the heading: at least two non-empty segments separated by "::" (i.e., the H1::H2[::H3...] form). Top-level headings (no parent) are unreachable through this tool — use get_file_contents + put_content for those edits. Headings whose literal text contains "::" are also unreachable through this tool (the validator treats every "::" as a path separator and there is no escape syntax).
Inputs: target (required), content (required), filepath (required), operation (required), +2 more.
pattern_searchB
Regex pattern extraction with context. Use for structured data mining, format analysis, content auditing.
Inputs: patterns (required), scope, options, vaultId.
put_contentA
Overwrite the entire content of a file. Creates the file if it does not exist.
Inputs: content (required), filepath (required), vaultId.
searchC
Performs a keyword search across all files in the vault.
Inputs: query (required), vaultId, contextLength.
semantic_searchB
Concept-based search via Smart Connections. Finds conceptually related content using meaning/context similarity rather than keyword matching.
Inputs: query (required), limit, filters, vaultId, +1 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 32 tools
Each tool targets a distinct resource and operation: file reading/writing, search with different query types, vault graph analysis, commands, and periodic notes. The descriptions clearly differentiate similar-looking tools like search, complex_search, pattern_search, and semantic_search.
Tool names follow a consistent lowercase snake_case convention with a verb-noun structure (put_content, get_file_contents, delete_file, list_tags). Even compound names like batch_get_file_contents and find_and_replace fit the pattern, and search variants share a common suffix.
With 32 tools, the server exceeds the typical well-scoped range (3-15) and enters the 'too many' category (25+). While each tool is distinct, the sheer number could overwhelm agents, and some tools could be parameterized to reduce count.
The surface covers CRUD, search (keyword, regex, semantic, JsonLogic), graph analysis, periodic notes, tags, and command execution. Minor gaps include rename/move operations and explicit folder creation, but the core workflows are well covered.
Related MCP Connectors
Hosted MCP server for Obsidian Sync vaults: read, search, edit notes. Not affiliated with Obsidian.
A TypeScript MCP server for Home Assistant, enabling programmatic management of entities, automati…
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
Related MCP Servers
- AlicenseAqualityBmaintenanceThe most feature-complete MCP server for Obsidian vaults. 23 tools and 3 resources for search, read, write, tags, link analysis, graph traversal, and canvas support.41103 npm34MIT
- AlicenseBqualityCmaintenanceHeadless semantic MCP server for Obsidian, Logseq, Dendron, Foam, and any markdown folder. Features built-in hybrid semantic search, surgical AST editing, template scaffolding, zero-config local embeddings, and workflow tracking.586 npm12MIT
- AlicenseAqualityCmaintenanceLocal-first MCP server for working with an Obsidian vault. No API key required1719MIT
- AlicenseNot gradedqualityBmaintenanceMCP server for Obsidian that exposes tools for reading/writing notes, managing frontmatter and tags, querying Tasks, semantic search, and interacting with Obsidian Bases, with shared local caching and support for various runtime modes.40Apache 2.0