Python client for the M-Files gRPC API: the protocol M-Files' own clients use.
It reaches parts of the vault the REST API (MFWS) does not, most usefully the
metadata structure. POST /REST/structure/properties answers HTTP 405, while
gRPC has IRPCPropertyDefsAdmin.AddPropertyDef, AddObjectClass,
IRPCObjectTypesAdmin.AddObjectType and a declarative whole-structure
get/set (IRPCDeclarativeMetadataStructure).
Not a supported public API. The protocol comes from the M-Files Desktop client install and can change with any server update. Regenerate the stubs (see Source of the .proto) after upgrading, and run the tests.
| What | State |
|---|---|
gRPC on the REST host and port (443 on M-Files Cloud), path /MFiles.<Service>/<Method> |
verified live |
The .proto matches the server (live replies decode field for field) |
verified live |
Anonymous calls (GetServerCapabilities, GetPublicKeyAnonymous) |
verified live |
LogIn with user name and password → 48-byte session ID |
verified live |
| Using that session on later calls | verified live (2026-09-24) |
Connecting through a capturing proxy (address, ca-cert; Proxide) |
verified live (2026-09-25) |
SSO: reading the vault's OAuth settings (auth-config), anonymously |
verified live (2026-09-24) |
SSO: browser sign-in, LogIn with the token, session accepted |
verified live (2026-09-24) |
| Structure helpers, reading object properties | verified live |
Writing object properties (set_properties, with expected_version guard) |
verified live (2026-09-24) |
create_object, remove_properties, delete_object, destroy_object |
verified live (2026-09-24) |
Verified against an M-Files Cloud vault. scripts/live_object_test.py repeats
the object checks against your vault: it creates a throwaway object, changes it,
adds values, checks that a write aimed at an old version is refused, empties one
value and removes another, then destroys the object. It writes to the vault;
on failure it prints the ID it left behind. --keep skips the delete. You name
an object type and properties of your own; see --help. For example:
python scripts/live_object_test.py --config client-config.toml --object-type eBook \
--integer "Page count" --optional-integer "Publishing year" --multiline Source
What it established:
- Creating an object needs
value_metadataon every value (the helper adds it); without it the server answers "Type mismatch." - An object of a type that can have files cannot be created without
Single file(22). - A property the class lists cannot be removed, only set empty (null).
remove_propertiesworks only for properties the class does not list, such asKeywords(26).
LogIn returns a session ID, but none of the request messages has a field
for it: it travels in call metadata (HTTP/2 headers). The .proto does not
say so; the header names came from M-Files. The client adds these to every call:
| Header | Value |
|---|---|
mfiles-session-id-bin |
session_data.session_id from LogIn, unchanged (after logging in) |
mfiles-is-remote-call |
true |
mfiles-activity-id-bin |
a new GUID for each call, for tracing |
gRPC base64-encodes -bin values itself. A call that needs a session and does
not carry mfiles-session-id-bin fails with UNAUTHENTICATED; a REST
X-Authentication token is not accepted in its place.
mfiles-grpc check-session logs in and makes one read-only call to confirm it.
Human users sign in through the vault's identity provider, not with an M-Files
password. The server does not run that sign-in for a client: the client gets a
token from the IdP itself and hands it to LogIn. The steps below mirror the
M-Files web client (26.10, loginToServer / doPluginLogin / getConfig in
Common\Web\Public\mfapp-bundle-mfwebui.*.js).
-
Discovery, anonymous.
IRPCLogin.GetAuthenticationConfiguration, asked with bothvault_guidandhost_name, once withACCOUNT_TYPE_MFILESand then withACCOUNT_TYPE_WINDOWS. On M-Files Cloud only the Windows type answers. The plugin with assemblyMFiles.AuthenticationProviders.OAuthcarries the IdP settings as named values:ClientID,AuthorizationEndpoint,TokenEndpoint,Scope,RedirectURI,Resource,ClientSecret,UseAccessTokenInWeb. Itssystem_configurationgives the configuration scope (Scope, for example*:WINDOWS:).GetAuthenticationPluginsdoes not work here: anonymously it names the plugin but omits its configuration. -
Sign-in: authorization code with PKCE. Like M-Files Desktop, the redirect is
RedirectURI, orhttp://localhostwhen there is none. On M-Files Cloud it ishttp://localhost/signin-oidc. The IdP compares the redirect exactly, so the one-shot listener binds its exact port, which is 80 when none is given. It binds on both127.0.0.1and::1. The browser does the rest, including MFA. -
LogInwithAUTH_DATA_TYPE_PLUGIN, in one round trip:Field Value plugin.plugin_namethe plugin's nameplugin.configuration_scopesystem_configuration["Scope"]plugin.authentication_attempt_identifier.data32 zero bytes plugin.data_formatPLUGIN_AUTH_DATA_FORMAT_UNENCRYPTEDplugin.auth_dataone named value, Token, asDATATYPE_TEXTThe token is the ID token, or the access token when
UseAccessTokenInWebis true. M-Files Cloud sets it to true. The session that comes back is the same kind as a password login, so everything afterLogInis unchanged.
Configure it with:
[m-files.tool.common]
rest-api-url = "https://<vault>.cloudvault.m-files.com/REST/"
vault = "{GUID}" # no username or password
[m-files.tool.grpc]
auth = "sso"
# sso-token = "access" # if the server refuses the ID tokenmfiles-grpc auth-config shows what discovery found, anonymously. Check it
first. The redirect URI must be a loopback address. If the vault's IdP accepts
only the web client's /signin-oidc, a command line client cannot receive the
sign-in. In that case, put a token obtained elsewhere in the
MFILES_GRPC_TOKEN environment variable; it is used instead of the browser.
It goes in the environment rather than on the command line so that it does not
land in shell history or process listings.
On Linux, binding port 80 needs root or CAP_NET_BIND_SERVICE. Without either,
use MFILES_GRPC_TOKEN.
pip install mfiles-grpc
Python 3.11 or newer. For working on the package itself: pip install -e ".[dev]".
Settings are read from a TOML file, client-config.toml by default
(load_settings(path), mfiles-grpc --config path). The file holds the
password, so keep it out of version control and readable only by you
(chmod 600).
[m-files.tool.common]
rest-api-url = "https://<vault>.cloudvault.m-files.com/REST/"
vault = "{GUID}" # the vault's GUID, with braces
username = "..." # for auth = "password"
password = "..."The gRPC host and port are taken from rest-api-url (port 443 when the URL has
none). An optional [m-files.tool.grpc] section overrides them and chooses how
to log in:
[m-files.tool.grpc]
port = 443 # overrides the port in rest-api-url
address = "localhost:4443" # connect here instead, e.g. a capturing proxy
ca-cert = "proxide_ca.crt" # trust these root certificates (PEM) instead of the system's
auth = "sso" # "password" (default) or "sso"; see above
sso-token = "access" # optional: "id" or "access"To watch the calls in a man-in-the-middle proxy such as
Proxide, leave rest-api-url at the real vault
and set address and ca-cert:
proxide monitor -l 4443 -t <vault>.cloudvault.m-files.com:443
[m-files.tool.grpc]
address = "localhost:4443"
ca-cert = "/path/to/proxide_ca.crt"With address set only the connection goes there. The vault host is still the
name sent in the TLS handshake (SNI) and at login, and the name the certificate is
checked against. Proxide makes its certificate from that name and forwards it to
the vault. Do not point rest-api-url at the proxy instead: the handshake and
login would then name localhost, and the REST tools reading the same file would go
through the proxy too.
The capture contains the login request, password included; treat the proxy's log as secret. With SSO it contains the token instead, which is just as secret.
The object type (101) and object (214) below are examples; use your vault's.
from mfiles_grpc import Client, load_settings, objects, structure, values, pb
with Client.connect(load_settings()) as client:
source = structure.property_def_by_name(client, "Source", pb.DATATYPE_MULTI_LINE_TEXT)
print(objects.get_property_values(client, 101, 214))
objects.set_properties(client, 101, 214,
{source.id: values.multiline_text("…")},
expected_version=3)Every service in the .proto is available as client.stub("IRPC<Name>"),
with the common ones as attributes: client.objects, client.object_types,
client.property_defs, client.value_lists, client.search,
client.property_defs_admin, client.object_types_admin. Messages and enum
values are on pb (pb.SetPropertiesRequest, pb.DATATYPE_TEXT).
mfiles-grpc capabilities # anonymous; does the host speak gRPC?
mfiles-grpc auth-config # anonymous; the vault's SSO settings
mfiles-grpc login # are the credentials good?
mfiles-grpc check-session # is the session accepted?
mfiles-grpc structure # object types, classes, custom properties
objects.set_properties()always sendsremove_unspecified_properties=False. WithTrue,SetPropertiesdeletes every property not in the request; acrossSetPropertiesMultiplethat would wipe a library's metadata. Use the raw stub if you really mean it.expected_version=makes a write fail rather than land on a version newer than the one you read. Without it, the latest version is looked up first:GetPropertiesanswers "Not found" to theLATESTversion marker, so the helpers always send a real version number.values.text()refuses more than 100 characters. M-Files silently truncates single-line text at 100; usevalues.multiline_text().values.normalise_newlines(): M-Files stores multi-line text with CRLF, so compare read-backs only after normalising.ConnectionSettingsnever prints the password or the SSO token.
Unchanged by the protocol: Comment (33) is per-version and not carried to new
versions; lookup names fold ß to ss; the Windows client still fails on
paths over 260 characters.
The .proto file itself is not in this repository or the package; only the stubs
generated from it (src/mfiles_grpc/_generated/) are. It ships with every M-Files
Desktop client install, which is where to take it from:
| File | mfilesCombinedWithDataPush.proto |
| Taken from | M-Files Desktop client 26.9.16459.6, C:\Program Files\M-Files\26.9.16459.6\Common\Web\GRPC\proto\ |
| Date | 2026-09-23 |
| Size | 1053262 bytes |
| SHA-256 | fcdfe3e144871941436b1869c28a25047d92db16771b2c7d3c31dbc7e3edaf0a |
After a client upgrade, compare the new client's file against this hash:
sha256sum mfilesCombinedWithDataPush.proto # Linux
Get-FileHash -Algorithm SHA256 mfilesCombinedWithDataPush.proto # PowerShell
If it differs, copy the new file into this directory (git ignores *.proto),
regenerate the stubs, run the tests, and update this table.
python scripts/generate_stubs.py [path/to/mfilesCombinedWithDataPush.proto]
Defaults to mfilesCombinedWithDataPush.proto in this directory. The script makes the
generated import package-relative and escapes the Windows paths in M-Files'
comments that would otherwise raise SyntaxWarning on import.
pip install -e ".[dev]"
pytest
flake8 --max-line-length 120 --extend-exclude src/mfiles_grpc/_generated src tests scripts
Offline; they need no vault. scripts/live_object_test.py is the live check.
Versions are never set by hand: every pull request merged to main gets the next one.
-
The Tag workflow tags the merge commit with the next version. The pull request's labels choose the step:
Label Step Example none patch 1.2.3 → 1.2.4 minor-versionminor 1.2.3 → 1.3.0 major-versionmajor 1.2.3 → 2.0.0 The first tag is
v1.0.0. -
To release, create a GitHub release on that tag (Releases → Draft a new release). Publishing it starts the Publish workflow, which builds the package, publishes it to PyPI and attaches the wheel and the sdist to the release. A tag without a release is not published.
The package version comes from the tag (setuptools-scm),
so pyproject.toml holds none. Running Publish by hand on a branch publishes a .devN version
to TestPyPI.
MIT; see LICENSE.